From f7638ae1f1834fbb2ec400e97462386760b224a8 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 3 Sep 2025 17:03:33 +0200 Subject: [PATCH 1/5] Did a mega refactor to make clients more generic and reusable when more clients are connected to the insights application --- .../insights/common/client/ApiClient.java | 42 + .../common/client/ApiClientException.java | 10 + .../common/client/graphql/GraphQLClient.java | 130 ++ .../graphql/GraphQLClientException.java | 10 + .../client/graphql/GraphQLConnectionDTO.java | 10 + .../common/client/graphql/GraphQLNodeDTO.java | 7 + .../client/graphql/GraphQLPageInfoDTO.java | 6 + .../common/client/graphql/GraphQLQuery.java | 7 + .../common/client/rest/RestClient.java | 48 + .../client/rest/RestClientException.java | 10 + ...tializer.java => GitHubConfiguration.java} | 30 +- .../configuration/SnykConfiguration.java | 60 + .../properties/FetchProperties.java | 12 + .../properties/GitHubProperties.java | 1 - .../properties/SnykProperties.java | 15 + .../common/exception/ApiException.java | 9 + .../insights/github/GitHubClient.java | 317 ++--- .../github/GitHubClientException.java | 7 +- .../insights/github/GitHubEdgesDTO.java | 3 +- .../insights/github/GitHubNodeDTO.java | 3 - .../insights/github/GitHubNodesDTO.java | 6 + .../insights/github/GitHubPageInfo.java | 3 - .../insights/github/GitHubPaginationDTO.java | 5 - .../github/GitHubPrioritySingleSelectDTO.java | 3 +- .../insights/github/GitHubQueryConstants.java | 3 +- .../insights/graphql/GraphQLClient.java | 31 - .../insights/issue/IssueDTO.java | 6 +- .../insights/issue/IssueRepository.java | 36 +- .../insights/issue/IssueService.java | 4 +- .../issuePriority/IssuePriorityService.java | 3 - .../insights/release/ReleaseService.java | 3 +- .../insights/snyk/SnykClient.java | 31 + .../resources/application-local.properties | 8 +- .../application-production.properties | 8 +- .../src/main/resources/examples/.env-example | 8 - .../insights/client/ApiClientTest.java | 4 + .../insights/client/GraphQLClientTest.java | 4 + .../insights/client/RestClientTest.java | 4 + .../insights/github/GitHubClientTest.java | 1164 +++++++++-------- .../insights/issue/IssueServiceTest.java | 7 +- .../pullrequest/PullRequestServiceTest.java | 8 +- .../insights/shedlock/ShedLockLocalTest.java | 33 +- .../shedlock/ShedLockProductionTest.java | 30 +- .../insights/shedlock/ShedLockTest.java | 24 +- 44 files changed, 1268 insertions(+), 905 deletions(-) create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClient.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClientException.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLConnectionDTO.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLNodeDTO.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLPageInfoDTO.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLQuery.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClient.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClientException.java rename InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/{SystemDataInitializer.java => GitHubConfiguration.java} (85%) create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/FetchProperties.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/SnykProperties.java delete mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodeDTO.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodesDTO.java delete mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPageInfo.java delete mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPaginationDTO.java delete mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/graphql/GraphQLClient.java create mode 100644 InsightsBackend/src/main/java/org/frankframework/insights/snyk/SnykClient.java delete mode 100644 InsightsBackend/src/main/resources/examples/.env-example create mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java create mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java create mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClient.java new file mode 100644 index 00000000..14bf30c3 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClient.java @@ -0,0 +1,42 @@ +package org.frankframework.insights.common.client; + +import java.util.function.Consumer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * The abstract base class for all external API clients. + * It centralizes the creation and configuration of the Spring WebClient. + */ +@Slf4j +public abstract class ApiClient { + + protected final WebClient webClient; + + /** + * Flexible constructor for any WebClient-based client. + * @param baseUrl the base URL of the external server (mandatory). + * @param configurer a Consumer that configures the WebClient.Builder, e.g., for auth headers. + */ + public ApiClient(String baseUrl, Consumer configurer) { + if (baseUrl == null || baseUrl.trim().isEmpty()) { + throw new IllegalArgumentException("Base URL cannot be null or empty."); + } + + WebClient.Builder builder = WebClient.builder() + .baseUrl(baseUrl) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + if (configurer != null) { + configurer.accept(builder); + } + + this.webClient = builder.build(); + log.info( + "WebClient initialized successfully for {} with base URL: {}", + this.getClass().getSimpleName(), + baseUrl); + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClientException.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClientException.java new file mode 100644 index 00000000..1ac15a59 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/ApiClientException.java @@ -0,0 +1,10 @@ +package org.frankframework.insights.common.client; + +import org.frankframework.insights.common.exception.ApiException; +import org.springframework.http.HttpStatus; + +public class ApiClientException extends ApiException { + public ApiClientException(String message, HttpStatus status, Throwable cause) { + super(message, status, cause); + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java new file mode 100644 index 00000000..fdea12a7 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java @@ -0,0 +1,130 @@ +package org.frankframework.insights.common.client.graphql; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.frankframework.insights.common.client.ApiClient; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.graphql.client.HttpGraphQlClient; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +public abstract class GraphQLClient extends ApiClient { + private final HttpGraphQlClient graphQlClient; + private final ObjectMapper objectMapper; + + public GraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { + super(baseUrl, configurer); + this.graphQlClient = HttpGraphQlClient.builder(this.webClient).build(); + this.objectMapper = objectMapper; + } + + protected T fetchSingleEntity(GraphQLQuery query, Map queryVariables, Class entityType) + throws GraphQLClientException { + try { + return getGraphQlClient() + .documentName(query.getDocumentName()) + .variables(queryVariables) + .retrieve(query.getRetrievePath()) + .toEntity(entityType) + .block(); + } catch (Exception e) { + throw new GraphQLClientException("Failed GraphQL request for document: " + query.getDocumentName(), e); + } + } + + /** + * A flexible method to fetch paginated data, allowing the caller to define how collections are extracted. + * This can handle various GraphQL response structures, such as those with 'edges' or 'nodes'. + * + * @param query the GraphQL query constant. + * @param queryVariables the variables for the query. + * @param entityType the class type of the final entities. + * @param responseType the type reference for deserializing the raw GraphQL response. + * @param collectionExtractor a function to extract the collection of raw data maps from the response. + * @param pageInfoExtractor a function to extract pagination info from the response. + * @return a set of all fetched entities across all pages. + * @throws GraphQLClientException if the request fails. + */ + protected Set fetchPaginatedCollection( + GraphQLQuery query, + Map queryVariables, + Class entityType, + ParameterizedTypeReference responseType, + Function>> collectionExtractor, + Function pageInfoExtractor) + throws GraphQLClientException { + + Function> entityExtractor = response -> { + Collection> rawNodes = collectionExtractor.apply(response); + if (rawNodes == null) { + return Set.of(); + } + return rawNodes.stream() + .map(node -> objectMapper.convertValue(node, entityType)) + .collect(Collectors.toList()); + }; + + return fetchPaginated(query, queryVariables, responseType, entityExtractor, pageInfoExtractor); + } + + private Set fetchPaginated( + GraphQLQuery query, + Map queryVariables, + ParameterizedTypeReference responseType, + Function> entityExtractor, + Function pageInfoExtractor) + throws GraphQLClientException { + try { + Set allEntities = new HashSet<>(); + String cursor = null; + boolean hasNextPage = true; + + while (hasNextPage) { + queryVariables.put("after", cursor); + RAW response = getGraphQlClient() + .documentName(query.getDocumentName()) + .variables(queryVariables) + .retrieve(query.getRetrievePath()) + .toEntity(responseType) + .block(); + + if (response == null) { + log.warn("Received null response for query: {}", query); + break; + } + + Collection entities = entityExtractor.apply(response); + if (entities == null || entities.isEmpty()) { + log.warn("Received empty entities for query: {}", query); + break; + } + + allEntities.addAll(entities); + log.info("Fetched {} entities with query: {}", entities.size(), query); + + GraphQLPageInfoDTO pageInfo = pageInfoExtractor.apply(response); + hasNextPage = pageInfo != null && pageInfo.hasNextPage(); + cursor = (pageInfo != null) ? pageInfo.endCursor() : null; + } + log.info( + "Completed paginated fetch for [{}], total entities found: {}", + query.getDocumentName(), + allEntities.size()); + return allEntities; + } catch (Exception e) { + throw new GraphQLClientException( + "Failed paginated GraphQL request for document: " + query.getDocumentName(), e); + } + } + + private HttpGraphQlClient getGraphQlClient() { + return graphQlClient; + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java new file mode 100644 index 00000000..16b9193c --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java @@ -0,0 +1,10 @@ +package org.frankframework.insights.common.client.graphql; + +import org.frankframework.insights.common.client.ApiClientException; +import org.springframework.http.HttpStatus; + +public class GraphQLClientException extends ApiClientException { + public GraphQLClientException(String message, Throwable cause) { + super(message, HttpStatus.INTERNAL_SERVER_ERROR, cause); + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLConnectionDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLConnectionDTO.java new file mode 100644 index 00000000..8472e8e5 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLConnectionDTO.java @@ -0,0 +1,10 @@ +package org.frankframework.insights.common.client.graphql; + +import java.util.List; + +/** + * Represents a generic GraphQL "Connection" for cursor-based pagination. + * It holds a list of edges and the pagination information. + * @param the type of the entity within the connection's nodes. + */ +public record GraphQLConnectionDTO(List> edges, GraphQLPageInfoDTO pageInfo) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLNodeDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLNodeDTO.java new file mode 100644 index 00000000..2e2f98de --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLNodeDTO.java @@ -0,0 +1,7 @@ +package org.frankframework.insights.common.client.graphql; + +/** + * A generic container for a "node" in a GraphQL edge, part of the Relay cursor-based pagination spec. + * @param the type of the entity contained within the node. + */ +public record GraphQLNodeDTO(T node) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLPageInfoDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLPageInfoDTO.java new file mode 100644 index 00000000..982aa1eb --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLPageInfoDTO.java @@ -0,0 +1,6 @@ +package org.frankframework.insights.common.client.graphql; + +/** + * A concrete DTO for GraphQL PageInfo. + */ +public record GraphQLPageInfoDTO(boolean hasNextPage, String endCursor) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLQuery.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLQuery.java new file mode 100644 index 00000000..ccbbe111 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLQuery.java @@ -0,0 +1,7 @@ +package org.frankframework.insights.common.client.graphql; + +public interface GraphQLQuery { + String getDocumentName(); + + String getRetrievePath(); +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClient.java new file mode 100644 index 00000000..b2e892a9 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClient.java @@ -0,0 +1,48 @@ +package org.frankframework.insights.common.client.rest; + +import java.util.function.Consumer; +import lombok.extern.slf4j.Slf4j; +import org.frankframework.insights.common.client.ApiClient; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +public abstract class RestClient extends ApiClient { + /** + * Flexible constructor for RestClient. + * @param baseUrl the base URL of the external REST server (mandatory). + * @param configurer a Consumer that receives the WebClient.Builder for further configuration, such as adding authentication headers. Can be null. + */ + public RestClient(String baseUrl, Consumer configurer) { + super(baseUrl, configurer); + } + + /** + * Executes a GET request to a specified path. + * @param path The endpoint path to request. + * @param responseType The class or type reference of the expected response. + * @param The type of the response body. + * @return The deserialized response body. + * @throws RestClientException if the request fails. + */ + protected T get(String path, ParameterizedTypeReference responseType) throws RestClientException { + try { + return getRestClient() + .get() + .uri(path) + .retrieve() + .bodyToMono(responseType) + .block(); + } catch (Exception e) { + throw new RestClientException(String.format("Failed GET request to path: %s", path), e); + } + } + + /** + * Provides access to the configured WebClient instance. + * @return the WebClient instance. + */ + protected WebClient getRestClient() { + return this.webClient; + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClientException.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClientException.java new file mode 100644 index 00000000..e959d687 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/rest/RestClientException.java @@ -0,0 +1,10 @@ +package org.frankframework.insights.common.client.rest; + +import org.frankframework.insights.common.client.ApiClientException; +import org.springframework.http.HttpStatus; + +public class RestClientException extends ApiClientException { + public RestClientException(String message, Throwable cause) { + super(message, HttpStatus.INTERNAL_SERVER_ERROR, cause); + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SystemDataInitializer.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/GitHubConfiguration.java similarity index 85% rename from InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SystemDataInitializer.java rename to InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/GitHubConfiguration.java index 4bd018a8..842d7057 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SystemDataInitializer.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/GitHubConfiguration.java @@ -3,7 +3,7 @@ import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.frankframework.insights.branch.BranchService; -import org.frankframework.insights.common.configuration.properties.GitHubProperties; +import org.frankframework.insights.common.configuration.properties.FetchProperties; import org.frankframework.insights.github.GitHubClientException; import org.frankframework.insights.github.GitHubRepositoryStatisticsService; import org.frankframework.insights.issue.IssueService; @@ -19,7 +19,7 @@ @Configuration @Slf4j -public class SystemDataInitializer implements CommandLineRunner { +public class GitHubConfiguration implements CommandLineRunner { private final GitHubRepositoryStatisticsService gitHubRepositoryStatisticsService; private final LabelService labelService; private final MilestoneService milestoneService; @@ -29,9 +29,9 @@ public class SystemDataInitializer implements CommandLineRunner { private final IssueService issueService; private final PullRequestService pullRequestService; private final ReleaseService releaseService; - private final Boolean gitHubFetchEnabled; + private final Boolean fetchEnabled; - public SystemDataInitializer( + public GitHubConfiguration( GitHubRepositoryStatisticsService gitHubRepositoryStatisticsService, LabelService labelService, MilestoneService milestoneService, @@ -41,7 +41,7 @@ public SystemDataInitializer( IssueService issueService, PullRequestService pullRequestService, ReleaseService releaseService, - GitHubProperties gitHubProperties) { + FetchProperties fetchProperties) { this.gitHubRepositoryStatisticsService = gitHubRepositoryStatisticsService; this.labelService = labelService; this.milestoneService = milestoneService; @@ -51,7 +51,7 @@ public SystemDataInitializer( this.issueService = issueService; this.pullRequestService = pullRequestService; this.releaseService = releaseService; - this.gitHubFetchEnabled = gitHubProperties.getFetch(); + this.fetchEnabled = fetchProperties.getEnabled(); } /** @@ -63,8 +63,8 @@ public SystemDataInitializer( public void run(String... args) { log.info("Startup: Fetching GitHub statistics"); fetchGitHubStatistics(); - log.info("Startup: Fetching full system data"); - initializeSystemData(); + log.info("Startup: Fetching all GitHub data"); + initializeGitHubData(); } /** @@ -75,7 +75,7 @@ public void run(String... args) { public void dailyJob() { log.info("Daily fetch job started"); fetchGitHubStatistics(); - initializeSystemData(); + initializeGitHubData(); } /** @@ -84,7 +84,7 @@ public void dailyJob() { @SchedulerLock(name = "fetchGitHubStatistics", lockAtMostFor = "PT10M") public void fetchGitHubStatistics() { try { - if (!gitHubFetchEnabled) { + if (!fetchEnabled) { log.info("Skipping GitHub fetch: skipping due to build/test configuration."); return; } @@ -96,12 +96,12 @@ public void fetchGitHubStatistics() { } /** - * Initializes system data by fetching labels, milestones, branches, issues, pull requests, and releases from GitHub. + * Initializes data by fetching labels, milestones, branches, issues, pull requests, and releases from GitHub. */ - @SchedulerLock(name = "initializeSystemData", lockAtMostFor = "PT2H") - public void initializeSystemData() { + @SchedulerLock(name = "initializeGitHubData", lockAtMostFor = "PT2H") + public void initializeGitHubData() { try { - if (!gitHubFetchEnabled) { + if (!fetchEnabled) { log.info("Skipping GitHub fetch: skipping due to build/test configuration."); return; } @@ -118,7 +118,7 @@ public void initializeSystemData() { log.info("Done fetching all GitHub data"); } catch (Exception e) { - log.error("Error initializing system data", e); + log.error("Error initializing GitHub data", e); } } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java new file mode 100644 index 00000000..ada64cb4 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java @@ -0,0 +1,60 @@ +package org.frankframework.insights.common.configuration; + +import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.frankframework.insights.common.configuration.properties.FetchProperties; +import org.frankframework.insights.snyk.SnykClient; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@Slf4j +public class SnykConfiguration implements CommandLineRunner { + private final Boolean fetchEnabled; + + public SnykConfiguration(FetchProperties fetchProperties) { + this.fetchEnabled = fetchProperties.getEnabled(); + } + + /** + * CommandLineRunner method that runs on application startup. + * @param args command line arguments + */ + @Override + @SchedulerLock(name = "startUpSnykUpdate", lockAtMostFor = "PT2H", lockAtLeastFor = "PT30M") + public void run(String... args) { + log.info("Startup: Fetching all Snyk data"); + initializeSnykData(); + } + + /** + * Scheduled job that runs daily at midnight. + */ + @Scheduled(cron = "0 0 0 * * *") + @SchedulerLock(name = "dailySnykUpdate", lockAtMostFor = "PT2H", lockAtLeastFor = "PT30M") + public void dailyJob() { + log.info("Daily fetch job started"); + initializeSnykData(); + } + + /** + * Initializes data by fetching CVE'S from Snyk.io. + */ + @SchedulerLock(name = "initializeSnykData", lockAtMostFor = "PT2H") + public void initializeSnykData() { + try { + if (!fetchEnabled) { + log.info("Skipping Snyk fetch: skipping due to build/test configuration."); + return; + } + + log.info("Start fetching all Snyk data"); + // todo add services with logic to fetch data from Snyk.io + + log.info("Done fetching all Snyk data"); + } catch (Exception e) { + log.error("Error initializing Snyk data", e); + } + } +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/FetchProperties.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/FetchProperties.java new file mode 100644 index 00000000..b343440e --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/FetchProperties.java @@ -0,0 +1,12 @@ +package org.frankframework.insights.common.configuration.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "fetch") +@Getter +@Setter +public class FetchProperties { + private Boolean enabled; +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/GitHubProperties.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/GitHubProperties.java index b677447a..29bf6fda 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/GitHubProperties.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/GitHubProperties.java @@ -15,5 +15,4 @@ public class GitHubProperties { private List branchProtectionRegexes; private List priorityLabels; private List ignoredLabels; - private Boolean fetch; } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/SnykProperties.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/SnykProperties.java new file mode 100644 index 00000000..6f36e303 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/properties/SnykProperties.java @@ -0,0 +1,15 @@ +package org.frankframework.insights.common.configuration.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "snyk.api") +@Getter +@Setter +public class SnykProperties { + private String url; + private String token; + private String orgId; + private String version; +} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/exception/ApiException.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/exception/ApiException.java index 42c2c947..6e58063a 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/exception/ApiException.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/exception/ApiException.java @@ -3,6 +3,11 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +/** + * The root of the custom exception hierarchy for the application. + * This is a checked exception, forcing callers to handle potential API-level + * errors explicitly. + */ @Getter public class ApiException extends Exception { private final HttpStatus statusCode; @@ -11,4 +16,8 @@ public ApiException(String message, HttpStatus statusCode, Throwable cause) { super(message, cause); this.statusCode = statusCode; } + + public ApiException(String message, Throwable cause) { + this(message, HttpStatus.INTERNAL_SERVER_ERROR, cause); + } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java index 209c8881..ae4e433f 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java @@ -3,15 +3,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.frankframework.insights.branch.BranchDTO; +import org.frankframework.insights.common.client.graphql.GraphQLClient; +import org.frankframework.insights.common.client.graphql.GraphQLClientException; +import org.frankframework.insights.common.client.graphql.GraphQLConnectionDTO; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; +import org.frankframework.insights.common.client.graphql.GraphQLQuery; import org.frankframework.insights.common.configuration.properties.GitHubProperties; -import org.frankframework.insights.graphql.GraphQLClient; import org.frankframework.insights.issue.IssueDTO; import org.frankframework.insights.issuetype.IssueTypeDTO; import org.frankframework.insights.label.LabelDTO; @@ -19,19 +22,39 @@ import org.frankframework.insights.pullrequest.PullRequestDTO; import org.frankframework.insights.release.ReleaseDTO; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; -/** - * GitHubClient is a GraphQL client for interacting with the GitHub API. - */ @Component @Slf4j public class GitHubClient extends GraphQLClient { - private final ObjectMapper objectMapper; public GitHubClient(GitHubProperties gitHubProperties, ObjectMapper objectMapper) { - super(gitHubProperties.getUrl(), gitHubProperties.getSecret()); - this.objectMapper = objectMapper; + super( + gitHubProperties.getUrl(), + builder -> { + if (gitHubProperties.getSecret() != null + && !gitHubProperties.getSecret().isEmpty()) { + builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + gitHubProperties.getSecret()); + } + }, + objectMapper); + } + + /** + * Fetches repository statistics from GitHub. + * @return GitHubRepositoryStatisticsDTO containing repository statistics + * @throws GitHubClientException if an error occurs during the request + */ + public GitHubRepositoryStatisticsDTO getRepositoryStatistics() throws GitHubClientException { + try { + GitHubRepositoryStatisticsDTO repositoryStatisticsDTO = fetchSingleEntity( + GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); + log.info("Fetched repository statistics from GitHub"); + return repositoryStatisticsDTO; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch repository statistics from GitHub.", e); + } } /** @@ -40,9 +63,13 @@ public GitHubClient(GitHubProperties gitHubProperties, ObjectMapper objectMapper * @throws GitHubClientException if an error occurs during the request */ public Set getLabels() throws GitHubClientException { - Set labels = getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); - log.info("Successfully fetched {} labels from GitHub", labels.size()); - return labels; + try { + Set labels = fetchPaginatedViaRelay(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); + log.info("Successfully fetched {} labels from GitHub", labels.size()); + return labels; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch labels from GitHub.", e); + } } /** @@ -51,10 +78,14 @@ public Set getLabels() throws GitHubClientException { * @throws GitHubClientException if an error occurs during the request */ public Set getMilestones() throws GitHubClientException { - Set milestones = - getEntities(GitHubQueryConstants.MILESTONES, new HashMap<>(), MilestoneDTO.class); - log.info("Successfully fetched {} milestones from GitHub", milestones.size()); - return milestones; + try { + Set milestones = + fetchPaginatedViaRelay(GitHubQueryConstants.MILESTONES, new HashMap<>(), MilestoneDTO.class); + log.info("Successfully fetched {} milestones from GitHub", milestones.size()); + return milestones; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch milestones from GitHub.", e); + } } /** @@ -63,10 +94,14 @@ public Set getMilestones() throws GitHubClientException { * @throws GitHubClientException of an error occurs during the request */ public Set getIssueTypes() throws GitHubClientException { - Set issueTypes = - getEntities(GitHubQueryConstants.ISSUE_TYPES, new HashMap<>(), IssueTypeDTO.class); - log.info("Successfully fetched {} issue types from GitHub", issueTypes.size()); - return issueTypes; + try { + Set issueTypes = + fetchPaginatedViaRelay(GitHubQueryConstants.ISSUE_TYPES, new HashMap<>(), IssueTypeDTO.class); + log.info("Successfully fetched {} issueTypes from GitHub", issueTypes.size()); + return issueTypes; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issue types from GitHub.", e); + } } /** @@ -77,19 +112,16 @@ public Set getIssueTypes() throws GitHubClientException { */ public Set getIssuePriorities(String projectId) throws GitHubClientException { - HashMap variables = new HashMap<>(); - variables.put("projectId", projectId); - log.info("Started fetching issue priorities from GitHub for project with id: [{}]", projectId); - - Set issuePriorities = - getNodes(GitHubQueryConstants.ISSUE_PRIORITIES, variables, new ParameterizedTypeReference<>() {}); - - log.info( - "Successfully fetched {} issue priorities from GitHub for project with id: [{}]", - issuePriorities.size(), - projectId); - - return issuePriorities; + try { + Map variables = new HashMap<>(); + variables.put("projectId", projectId); + return fetchPaginatedViaNodes( + GitHubQueryConstants.ISSUE_PRIORITIES, + variables, + GitHubPrioritySingleSelectDTO.SingleSelectObject.class); + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issue priorities from GitHub.", e); + } } /** @@ -98,9 +130,14 @@ public Set getIssuePriorities( * @throws GitHubClientException if an error occurs during the request */ public Set getBranches() throws GitHubClientException { - Set branches = getEntities(GitHubQueryConstants.BRANCHES, new HashMap<>(), BranchDTO.class); - log.info("Successfully fetched {} branches from GitHub", branches.size()); - return branches; + try { + Set branches = + fetchPaginatedViaRelay(GitHubQueryConstants.BRANCHES, new HashMap<>(), BranchDTO.class); + log.info("Successfully fetched {} branches from GitHub", branches.size()); + return branches; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch branches from GitHub.", e); + } } /** @@ -109,9 +146,13 @@ public Set getBranches() throws GitHubClientException { * @throws GitHubClientException if an error occurs during the request */ public Set getIssues() throws GitHubClientException { - Set issues = getEntities(GitHubQueryConstants.ISSUES, new HashMap<>(), IssueDTO.class); - log.info("Successfully fetched {} issues from GitHub", issues.size()); - return issues; + try { + Set issues = fetchPaginatedViaRelay(GitHubQueryConstants.ISSUES, new HashMap<>(), IssueDTO.class); + log.info("Successfully fetched {} issues from GitHub", issues.size()); + return issues; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issues from GitHub.", e); + } } /** @@ -121,172 +162,68 @@ public Set getIssues() throws GitHubClientException { * @throws GitHubClientException if an error occurs during the request */ public Set getBranchPullRequests(String branchName) throws GitHubClientException { - HashMap variables = new HashMap<>(); - variables.put("branchName", branchName); - log.info("Started fetching pull requests from GitHub for branch with name: {}", branchName); - - Set pullRequests = - getEntities(GitHubQueryConstants.BRANCH_PULLS, variables, PullRequestDTO.class); - log.info( - "Successfully fetched {} pull requests from GitHub for branch with name: {}", - pullRequests.size(), - branchName); - return pullRequests; + try { + Map variables = new HashMap<>(); + variables.put("branchName", branchName); + Set pullRequests = + fetchPaginatedViaRelay(GitHubQueryConstants.BRANCH_PULLS, variables, PullRequestDTO.class); + log.info( + "Successfully fetched {} pull requests for branch {} from GitHub", pullRequests.size(), branchName); + return pullRequests; + } catch (GraphQLClientException e) { + throw new GitHubClientException( + String.format("Failed to fetch pull requests for branch '%s' from GitHub.", branchName), e); + } } - /** - * Fetches releases from GitHub. - * @return Set of ReleaseDTO containing releases - * @throws GitHubClientException if an error occurs during the request - */ public Set getReleases() throws GitHubClientException { - Set releases = getEntities(GitHubQueryConstants.RELEASES, new HashMap<>(), ReleaseDTO.class); - log.info("Successfully fetched {} releases from GitHub", releases.size()); - return releases; - } - - /** - * Fetches entities from GitHub using a GraphQL query. - * @param query the GraphQL query to execute - * @param queryVariables the variables for the query - * @param entityType the type of entity to fetch - * @return Set of entities of the specified type - * @param the type of entity - * @throws GitHubClientException if an error occurs during the request - */ - protected Set getEntities( - GitHubQueryConstants query, Map queryVariables, Class entityType) - throws GitHubClientException { - return getPaginatedEntities( - query, - queryVariables, - new ParameterizedTypeReference>() {}, - dto -> dto.edges() == null - ? Set.of() - : dto.edges().stream() - .map(edge -> objectMapper.convertValue(edge.node(), entityType)) - .collect(Collectors.toSet()), - GitHubPaginationDTO::pageInfo); - } - - /** - * Fetches nodes from GitHub using a GraphQL query. - * @param query the GraphQL query to execute - * @param queryVariables the variables for the query - * @param responseType the type of the response to expect - * @return Set of GitHubSingleSelectDTO.SingleSelectObject containing nodes - * @param the raw response type - * @throws GitHubClientException if an error occurs during the request - */ - protected - Set getNodes( - GitHubQueryConstants query, - Map queryVariables, - ParameterizedTypeReference responseType) - throws GitHubClientException { - return getPaginatedEntities( - query, - queryVariables, - responseType, - dto -> dto.nodes() == null ? Set.of() : new HashSet<>(dto.nodes()), - GitHubPrioritySingleSelectDTO::pageInfo); + try { + Set releases = + fetchPaginatedViaRelay(GitHubQueryConstants.RELEASES, new HashMap<>(), ReleaseDTO.class); + log.info("Successfully fetched {} releases from GitHub", releases.size()); + return releases; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch releases from GitHub.", e); + } } /** - * Executes a GraphQL query to fetch paginated entities from GitHub. + * Helper to fetch paginated data from a standard Relay-style GraphQL connection. * @param query the GraphQL query to execute - * @param queryVariables the variables for the query - * @param responseType the type of the response to expect - * @param entityExtractor a function to extract entities from the response - * @param pageInfoExtractor a function to extract pagination information from the response - * @return Set of entities of the specified type - * @param the raw response type - * @param the type of entity to fetch - * @throws GitHubClientException if an error occurs during the request + * @param queryVariables the variables for the GraphQL query + * @param entityType the class type of the entities to fetch + * @return Set of entities of type T + * @param the type of entities to fetch + * @throws GraphQLClientException if an error occurs during the request */ - protected Set getPaginatedEntities( - GitHubQueryConstants query, - Map queryVariables, - ParameterizedTypeReference responseType, - Function> entityExtractor, - Function pageInfoExtractor) - throws GitHubClientException { - - try { - Set allEntities = new HashSet<>(); - String cursor = null; - boolean hasNextPage = true; - - while (hasNextPage) { - queryVariables.put("after", cursor); - - RAW response = getGraphQlClient() - .documentName(query.getDocumentName()) - .variables(queryVariables) - .retrieve(query.getRetrievePath()) - .toEntity(responseType) - .block(); + private Set fetchPaginatedViaRelay( + GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; - if (response == null) { - log.warn("Received null response for query: {}", query); - break; - } - - Collection entities = entityExtractor.apply(response); - if (entities == null || entities.isEmpty()) { - log.warn("Received empty entities for query: {}", query); - break; - } - - allEntities.addAll(entities); - log.info("Fetched {} entities with query: {}", entities.size(), query); + Function>, Collection>> collectionExtractor = + connection -> connection.edges() == null + ? Set.of() + : connection.edges().stream().map(GraphQLNodeDTO::node).collect(Collectors.toList()); - GitHubPageInfo pageInfo = pageInfoExtractor.apply(response); - hasNextPage = pageInfo != null && pageInfo.hasNextPage(); - cursor = (pageInfo != null) ? pageInfo.endCursor() : null; - } - return allEntities; - } catch (Exception e) { - throw new GitHubClientException("Failed to execute GraphQL request for " + query, e); - } + return fetchPaginatedCollection( + query, queryVariables, entityType, responseType, collectionExtractor, GraphQLConnectionDTO::pageInfo); } /** - * Fetches repository statistics from GitHub. - * @return GitHubRepositoryStatisticsDTO containing repository statistics - * @throws GitHubClientException if an error occurs during the request - */ - public GitHubRepositoryStatisticsDTO getRepositoryStatistics() throws GitHubClientException { - GitHubRepositoryStatisticsDTO repositoryStatistics = fetchSingleEntity( - GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); - log.info("Fetched repository statistics from GitHub"); - return repositoryStatistics; - } - - /** - * Executes a GraphQL query to fetch a single entity from GitHub. + * Helper to fetch paginated data from a GraphQL connection using the 'nodes' field. * @param query the GraphQL query to execute - * @param queryVariables the variables for the query - * @param entityType the type of entity to fetch - * @return the entity of the specified type - * @param the type of entity - * @throws GitHubClientException if an error occurs during the request + * @param queryVariables the variables for the GraphQL query + * @param entityType the class type of the entities to fetch + * @return Set of entities of type T + * @param the type of entities to fetch + * @throws GraphQLClientException if an error occurs during the request */ - protected T fetchSingleEntity( - GitHubQueryConstants query, Map queryVariables, Class entityType) - throws GitHubClientException { - try { - T response = getGraphQlClient() - .documentName(query.getDocumentName()) - .variables(queryVariables) - .retrieve(query.getRetrievePath()) - .toEntity(entityType) - .block(); - - log.info("Successfully requested single object with GraphQL query: {}", query); - return response; - } catch (Exception e) { - throw new GitHubClientException("Failed to execute GraphQL request for " + query, e); - } + private Set fetchPaginatedViaNodes( + GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + return fetchPaginatedCollection( + query, queryVariables, entityType, responseType, GitHubNodesDTO::nodes, GitHubNodesDTO::pageInfo); } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClientException.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClientException.java index 5dfd1026..889da03b 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClientException.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClientException.java @@ -1,10 +1,9 @@ package org.frankframework.insights.github; -import org.frankframework.insights.common.exception.ApiException; -import org.springframework.http.HttpStatus; +import org.frankframework.insights.common.client.graphql.GraphQLClientException; -public class GitHubClientException extends ApiException { +public class GitHubClientException extends GraphQLClientException { public GitHubClientException(String message, Throwable cause) { - super(message, HttpStatus.INTERNAL_SERVER_ERROR, cause); + super(message, cause); } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubEdgesDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubEdgesDTO.java index 6afbd9c9..97f5d4ed 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubEdgesDTO.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubEdgesDTO.java @@ -1,5 +1,6 @@ package org.frankframework.insights.github; import java.util.List; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; -public record GitHubEdgesDTO(List> edges) {} +public record GitHubEdgesDTO(List> edges) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodeDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodeDTO.java deleted file mode 100644 index 58cbe317..00000000 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodeDTO.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.frankframework.insights.github; - -public record GitHubNodeDTO(T node) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodesDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodesDTO.java new file mode 100644 index 00000000..34125530 --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubNodesDTO.java @@ -0,0 +1,6 @@ +package org.frankframework.insights.github; + +import java.util.List; +import org.frankframework.insights.common.client.graphql.GraphQLPageInfoDTO; + +public record GitHubNodesDTO(List nodes, GraphQLPageInfoDTO pageInfo) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPageInfo.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPageInfo.java deleted file mode 100644 index 2113758a..00000000 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPageInfo.java +++ /dev/null @@ -1,3 +0,0 @@ -package org.frankframework.insights.github; - -public record GitHubPageInfo(boolean hasNextPage, String endCursor) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPaginationDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPaginationDTO.java deleted file mode 100644 index 1b1e80e1..00000000 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPaginationDTO.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.frankframework.insights.github; - -import java.util.List; - -public record GitHubPaginationDTO(List> edges, GitHubPageInfo pageInfo) {} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPrioritySingleSelectDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPrioritySingleSelectDTO.java index 88cfa5d3..d52058ec 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPrioritySingleSelectDTO.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubPrioritySingleSelectDTO.java @@ -1,9 +1,10 @@ package org.frankframework.insights.github; import java.util.List; +import org.frankframework.insights.common.client.graphql.GraphQLPageInfoDTO; import org.frankframework.insights.issuePriority.IssuePriorityDTO; public record GitHubPrioritySingleSelectDTO( - List nodes, GitHubPageInfo pageInfo) { + List nodes, GraphQLPageInfoDTO pageInfo) { public record SingleSelectObject(String name, List options) {} } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubQueryConstants.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubQueryConstants.java index c54ad963..369fc311 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubQueryConstants.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubQueryConstants.java @@ -2,10 +2,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import org.frankframework.insights.common.client.graphql.GraphQLQuery; @Getter @AllArgsConstructor -public enum GitHubQueryConstants { +public enum GitHubQueryConstants implements GraphQLQuery { REPOSITORY_STATISTICS("repositoryStatistics", "repository"), LABELS("labels", "repository.labels"), MILESTONES("milestones", "repository.milestones"), diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/graphql/GraphQLClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/graphql/GraphQLClient.java deleted file mode 100644 index a0ac1000..00000000 --- a/InsightsBackend/src/main/java/org/frankframework/insights/graphql/GraphQLClient.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.frankframework.insights.graphql; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.graphql.client.HttpGraphQlClient; -import org.springframework.http.HttpHeaders; -import org.springframework.web.reactive.function.client.WebClient; - -@Slf4j -public abstract class GraphQLClient { - private final HttpGraphQlClient graphQlClient; - - /** - * Constructor for GraphQLClient. - * @param baseUrl the base URL of the external GraphQL server - * @param secret the secret token for authentication - */ - public GraphQLClient(String baseUrl, String secret) { - WebClient webClient = WebClient.builder() - .baseUrl(baseUrl) - .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + secret) - .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json") - .build(); - - this.graphQlClient = HttpGraphQlClient.builder(webClient).build(); - log.info("GraphQLClient initialized successfully with base URL: {}", baseUrl); - } - - protected HttpGraphQlClient getGraphQlClient() { - return graphQlClient; - } -} diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueDTO.java b/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueDTO.java index 61947128..95616dac 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueDTO.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueDTO.java @@ -4,8 +4,8 @@ import java.time.OffsetDateTime; import java.util.Objects; import java.util.Optional; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; import org.frankframework.insights.github.GitHubEdgesDTO; -import org.frankframework.insights.github.GitHubNodeDTO; import org.frankframework.insights.github.GitHubProjectItemDTO; import org.frankframework.insights.github.GitHubPropertyState; import org.frankframework.insights.issuetype.IssueTypeDTO; @@ -62,13 +62,13 @@ private Optional findProjectField( } return projectItems.edges().stream() - .map(GitHubNodeDTO::node) + .map(GraphQLNodeDTO::node) .map(GitHubProjectItemDTO::fieldValues) .flatMap(fv -> { if (fv.edges() == null || fv.edges().isEmpty()) return java.util.stream.Stream.empty(); return fv.edges().stream(); }) - .map(GitHubNodeDTO::node) + .map(GraphQLNodeDTO::node) .filter(Objects::nonNull) .filter(fv -> fv.field() != null && fieldName.equalsIgnoreCase(fv.field().name())) diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueRepository.java b/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueRepository.java index 02022986..457ab900 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueRepository.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueRepository.java @@ -9,30 +9,30 @@ @Repository public interface IssueRepository extends JpaRepository { - /** - * This query involves multiple joins across different entities. For such complex - * cases, a custom @Query is more readable and maintainable than a very long - * derived method name. - */ - @Query( - """ + /** + * This query involves multiple joins across different entities. For such complex + * cases, a custom @Query is more readable and maintainable than a very long + * derived method name. + */ + @Query( + """ SELECT DISTINCT i FROM Issue i JOIN PullRequestIssue pri ON pri.issue = i JOIN ReleasePullRequest rpr ON rpr.pullRequest = pri.pullRequest WHERE rpr.release.id = :releaseId """) - Set findIssuesByReleaseId(@Param("releaseId") String releaseId); + Set findIssuesByReleaseId(@Param("releaseId") String releaseId); - /** - * Finds all distinct issues by traversing the milestone relationship and matching its ID. - * Spring Data JPA generates the query from this method name. - */ - Set findDistinctByMilestoneId(String milestoneId); + /** + * Finds all distinct issues by traversing the milestone relationship and matching its ID. + * Spring Data JPA generates the query from this method name. + */ + Set findDistinctByMilestoneId(String milestoneId); - /** - * Finds all distinct issues where the closedAt date is within a given range. - * Spring Data JPA generates the query from this method name. - */ - Set findDistinctByClosedAtBetween(OffsetDateTime start, OffsetDateTime end); + /** + * Finds all distinct issues where the closedAt date is within a given range. + * Spring Data JPA generates the query from this method name. + */ + Set findDistinctByClosedAtBetween(OffsetDateTime start, OffsetDateTime end); } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueService.java b/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueService.java index 778d8c2e..c2161ed7 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueService.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/issue/IssueService.java @@ -6,11 +6,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; import org.frankframework.insights.common.entityconnection.issuelabel.IssueLabel; import org.frankframework.insights.common.entityconnection.issuelabel.IssueLabelRepository; import org.frankframework.insights.common.mapper.Mapper; import org.frankframework.insights.github.GitHubClient; -import org.frankframework.insights.github.GitHubNodeDTO; import org.frankframework.insights.issuePriority.IssuePriority; import org.frankframework.insights.issuePriority.IssuePriorityResponse; import org.frankframework.insights.issuePriority.IssuePriorityService; @@ -163,7 +163,7 @@ private void assignSubIssuesToIssues(Set issues, Map is Set subIssues = issueDTO.subIssues().edges().stream() .filter(Objects::nonNull) - .map(GitHubNodeDTO::node) + .map(GraphQLNodeDTO::node) .map(node -> issueMap.get(node.id())) .filter(Objects::nonNull) .collect(Collectors.toSet()); diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/issuePriority/IssuePriorityService.java b/InsightsBackend/src/main/java/org/frankframework/insights/issuePriority/IssuePriorityService.java index 1d923d14..62998bc8 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/issuePriority/IssuePriorityService.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/issuePriority/IssuePriorityService.java @@ -1,6 +1,5 @@ package org.frankframework.insights.issuePriority; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.Map; import java.util.Set; @@ -20,7 +19,6 @@ public class IssuePriorityService { private final GitHubClient gitHubClient; private final Mapper mapper; private final String projectId; - private final ObjectMapper objectMapper = new ObjectMapper(); private static final String PRIORITY_FIELD_NAME = "priority"; @@ -46,7 +44,6 @@ public void injectIssuePriorities() throws IssuePriorityInjectionException { Set singleSelectDTOS = gitHubClient.getIssuePriorities(projectId); Set issuePriorities = fetchAndMapGithubPriorityOptions(singleSelectDTOS); - System.out.println(objectMapper.writeValueAsString(issuePriorities)); saveIssuePriorities(issuePriorities); } catch (Exception e) { throw new IssuePriorityInjectionException("Error while injecting GitHub issue priorities", e); diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/release/ReleaseService.java b/InsightsBackend/src/main/java/org/frankframework/insights/release/ReleaseService.java index f1058f34..5ba9a654 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/release/ReleaseService.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/release/ReleaseService.java @@ -11,6 +11,7 @@ import org.frankframework.insights.common.entityconnection.branchpullrequest.BranchPullRequest; import org.frankframework.insights.common.entityconnection.releasepullrequest.ReleasePullRequest; import org.frankframework.insights.common.entityconnection.releasepullrequest.ReleasePullRequestRepository; +import org.frankframework.insights.common.exception.ApiException; import org.frankframework.insights.common.mapper.Mapper; import org.frankframework.insights.github.GitHubClient; import org.frankframework.insights.pullrequest.PullRequest; @@ -90,7 +91,7 @@ public void injectReleases() throws ReleaseInjectionException { .collect(Collectors.toList())))); processAndAssignPullsAndCommits(releasesByBranch, pullRequestsByBranch); - } catch (Exception e) { + } catch (ApiException e) { throw new ReleaseInjectionException("Error injecting GitHub releases.", e); } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/snyk/SnykClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/snyk/SnykClient.java new file mode 100644 index 00000000..5064850d --- /dev/null +++ b/InsightsBackend/src/main/java/org/frankframework/insights/snyk/SnykClient.java @@ -0,0 +1,31 @@ +package org.frankframework.insights.snyk; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.frankframework.insights.common.client.rest.RestClient; +import org.frankframework.insights.common.configuration.properties.SnykProperties; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class SnykClient extends RestClient { + private final String organisationId; + private final ObjectMapper objectMapper; + + /** + * Constructor that initializes the SnykClient with configuration properties. + * @param snykProperties The properties containing Snyk API configuration such as URL, token, organization ID, and version. + */ + public SnykClient(SnykProperties snykProperties, ObjectMapper objectMapper) { + super(snykProperties.getUrl(), builder -> { + if (snykProperties.getToken() != null && !snykProperties.getToken().isEmpty()) { + builder.defaultHeader(HttpHeaders.AUTHORIZATION, "token " + snykProperties.getToken()); + } + }); + this.organisationId = snykProperties.getOrgId(); + this.objectMapper = objectMapper; + } + + // todo add Snyk API calls for service here +} diff --git a/InsightsBackend/src/main/resources/application-local.properties b/InsightsBackend/src/main/resources/application-local.properties index 0520c731..fc15cf36 100644 --- a/InsightsBackend/src/main/resources/application-local.properties +++ b/InsightsBackend/src/main/resources/application-local.properties @@ -6,9 +6,15 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/insights spring.datasource.username=postgres spring.datasource.password=postgres +fetch.enabled=false + github.secret= -github.fetch=false github.projectId= +snyk.api.url= +snyk.api.token= +snyk.api.org-id= +snyk.api.version= + cors.allowed.origins[0]=http://localhost:4200 cors.allowed.origins[1]=http://localhost diff --git a/InsightsBackend/src/main/resources/application-production.properties b/InsightsBackend/src/main/resources/application-production.properties index 44855fc4..d954f405 100644 --- a/InsightsBackend/src/main/resources/application-production.properties +++ b/InsightsBackend/src/main/resources/application-production.properties @@ -6,9 +6,15 @@ spring.datasource.url=jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATA spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} +fetch.enabled=true + github.secret=${GITHUB_API_SECRET} -github.fetch=true github.projectId=${GITHUB_PROJECT_ID} +snyk.api.url=${SNYK_API_URL} +snyk.api.token=${SNYK_API_TOKEN} +snyk.api.org-id=${SNYK_API_ORG_ID} +snyk.api.version=${SNYK_API_VERSION} + cors.allowed.origins[0]=http://localhost cors.allowed.origins[1]=https://insights.frankframework.org diff --git a/InsightsBackend/src/main/resources/examples/.env-example b/InsightsBackend/src/main/resources/examples/.env-example deleted file mode 100644 index 5c9aae67..00000000 --- a/InsightsBackend/src/main/resources/examples/.env-example +++ /dev/null @@ -1,8 +0,0 @@ -DATABASE_HOST: -DATABASE_PORT: -DATABASE_NAME: -DATABASE_USERNAME: -DATABASE_PASSWORD: - -GITHUB_API_SECRET: -GITHUB_PROJECT_ID: diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java new file mode 100644 index 00000000..f05b6a89 --- /dev/null +++ b/InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java @@ -0,0 +1,4 @@ +package org.frankframework.insights.client; + +public class ApiClientTest { +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java new file mode 100644 index 00000000..c2fa955c --- /dev/null +++ b/InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java @@ -0,0 +1,4 @@ +package org.frankframework.insights.client; + +public class GraphQLClientTest { +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java new file mode 100644 index 00000000..345fb565 --- /dev/null +++ b/InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java @@ -0,0 +1,4 @@ +package org.frankframework.insights.client; + +public class RestClientTest { +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java index 40e8b132..57a9cf76 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java @@ -1,578 +1,586 @@ -package org.frankframework.insights.github; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.*; -import org.frankframework.insights.branch.BranchDTO; -import org.frankframework.insights.common.configuration.properties.GitHubProperties; -import org.frankframework.insights.issue.IssueDTO; -import org.frankframework.insights.issuePriority.IssuePriorityDTO; -import org.frankframework.insights.issuetype.IssueTypeDTO; -import org.frankframework.insights.label.LabelDTO; -import org.frankframework.insights.milestone.MilestoneDTO; -import org.frankframework.insights.pullrequest.PullRequestDTO; -import org.frankframework.insights.release.ReleaseDTO; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.*; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.client.HttpGraphQlClient; -import reactor.core.publisher.Mono; - -public class GitHubClientTest { - - @Mock - private ObjectMapper objectMapper; - - @Mock - private GitHubProperties gitHubProperties; - - @Mock - private HttpGraphQlClient httpGraphQlClient; - - private GitHubClient gitHubClient; - - private static class TestableGitHubClient extends GitHubClient { - private final HttpGraphQlClient testClient; - - public TestableGitHubClient(GitHubProperties props, ObjectMapper om, HttpGraphQlClient client) { - super(props, om); - this.testClient = client; - } - - @Override - protected HttpGraphQlClient getGraphQlClient() { - return testClient; - } - } - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - when(gitHubProperties.getUrl()).thenReturn("https://api.github.com"); - when(gitHubProperties.getSecret()).thenReturn("secret"); - } - - @Test - public void getLabels_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - LabelDTO label1 = new LabelDTO("id1", "Label 1", "Label 1 description", "red"); - LabelDTO label2 = new LabelDTO("id2", "Label 2", "Label 2 description", "blue"); - Set labels = Set.of(label1, label2); - doReturn(labels).when(gitHubClient).getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); - Set result = gitHubClient.getLabels(); - assertEquals(labels, result); - } - - @Test - public void getLabels_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); - assertTrue(gitHubClient.getLabels().isEmpty()); - } - - @Test - public void getLabels_nullReturned() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(null).when(gitHubClient).getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); - assertThrows(NullPointerException.class, () -> gitHubClient.getLabels()); - } - - @Test - public void getLabels_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getLabels()); - } - - @Test - public void getMilestones_success() throws Exception { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - MilestoneDTO m1 = new MilestoneDTO("id", 1, null, "https//example.com", GitHubPropertyState.OPEN, null, 0, 0); - Set milestones = Set.of(m1); - doReturn(milestones) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); - assertEquals(milestones, gitHubClient.getMilestones()); - } - - @Test - public void getMilestones_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); - assertTrue(gitHubClient.getMilestones().isEmpty()); - } - - @Test - public void getMilestones_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getMilestones()); - } - - @Test - public void getIssueTypes_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - IssueTypeDTO t1 = new IssueTypeDTO("id", "Bug", "A bug in the code", "bug"); - Set set = Set.of(t1); - doReturn(set) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); - assertEquals(set, gitHubClient.getIssueTypes()); - } - - @Test - public void getIssueTypes_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); - assertTrue(gitHubClient.getIssueTypes().isEmpty()); - } - - @Test - public void getIssueTypes_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getIssueTypes()); - } - - @Test - public void getIssuePriorities_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - String projectId = "pid"; - GitHubPrioritySingleSelectDTO.SingleSelectObject obj = - new GitHubPrioritySingleSelectDTO.SingleSelectObject("priority", null); - Set set = Set.of(obj); - - doReturn(set) - .when(gitHubClient) - .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); - - assertEquals(set, gitHubClient.getIssuePriorities(projectId)); - } - - @Test - public void getIssuePriorities_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - String projectId = "pid"; - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); - - assertTrue(gitHubClient.getIssuePriorities(projectId).isEmpty()); - } - - @Test - public void getIssuePriorities_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - String projectId = "pid"; - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); - - assertThrows(GitHubClientException.class, () -> gitHubClient.getIssuePriorities(projectId)); - } - - @Test - public void getBranches_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - BranchDTO branch = new BranchDTO("id", "main"); - Set set = Set.of(branch); - doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); - assertEquals(set, gitHubClient.getBranches()); - } - - @Test - public void getBranches_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); - assertTrue(gitHubClient.getBranches().isEmpty()); - } - - @Test - public void getBranches_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getBranches()); - } - - @Test - public void getIssues_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - - IssueDTO issue = new IssueDTO( - "id", - 1, - "Test Issue", - GitHubPropertyState.OPEN, - null, - "http://example.com", - new GitHubEdgesDTO<>(null), - null, - null, - new GitHubEdgesDTO<>(null), - new GitHubEdgesDTO<>(null)); - Set issues = Set.of(issue); - - doReturn(issues).when(gitHubClient).getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); - assertEquals(issues, gitHubClient.getIssues()); - } - - @Test - public void getIssues_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); - assertTrue(gitHubClient.getIssues().isEmpty()); - } - - @Test - public void getIssues_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getIssues()); - } - - @Test - public void getBranchPullRequests_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - PullRequestDTO pr = - new PullRequestDTO("id", 1, "Test PR", GitHubPropertyState.OPEN.name(), null, null, null, null); - Set prs = Set.of(pr); - doReturn(prs) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); - assertEquals(prs, gitHubClient.getBranchPullRequests("main")); - } - - @Test - public void getBranchPullRequests_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); - assertTrue(gitHubClient.getBranchPullRequests("main").isEmpty()); - } - - @Test - public void getBranchPullRequests_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getBranchPullRequests("main")); - } - - @Test - public void getReleases_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - ReleaseDTO dto = new ReleaseDTO("id", "v1.0", "Release 1.0", null); - Set set = Set.of(dto); - doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); - assertEquals(set, gitHubClient.getReleases()); - } - - @Test - public void getReleases_empty() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(Collections.emptySet()) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); - assertTrue(gitHubClient.getReleases().isEmpty()); - } - - @Test - public void getReleases_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getReleases()); - } - - @Test - public void getRepositoryStatistics_success() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - GitHubRepositoryStatisticsDTO stats = new GitHubRepositoryStatisticsDTO(null, null, null); - doReturn(stats) - .when(gitHubClient) - .fetchSingleEntity( - eq(GitHubQueryConstants.REPOSITORY_STATISTICS), - anyMap(), - eq(GitHubRepositoryStatisticsDTO.class)); - assertEquals(stats, gitHubClient.getRepositoryStatistics()); - } - - @Test - public void getRepositoryStatistics_exception() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doThrow(new GitHubClientException("fail", null)) - .when(gitHubClient) - .fetchSingleEntity( - eq(GitHubQueryConstants.REPOSITORY_STATISTICS), - anyMap(), - eq(GitHubRepositoryStatisticsDTO.class)); - assertThrows(GitHubClientException.class, () -> gitHubClient.getRepositoryStatistics()); - } - - @Test - public void getEntities_handlesNullEdgeCollection() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(null).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); - assertThrows(NullPointerException.class, () -> gitHubClient.getBranches()); - } - - @Test - public void getEntities_handlesNullQueryVariables() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - BranchDTO branch = new BranchDTO("id", "main"); - Set set = Set.of(branch); - doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), isNull(), eq(BranchDTO.class)); - } - - @Test - public void getNodes_handlesNullResult() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(null).when(gitHubClient).getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any()); - assertThrows(NullPointerException.class, () -> gitHubClient.getIssuePriorities("pid")); - } - - @Test - public void fetchSingleEntity_nullResponse() throws GitHubClientException { - gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); - doReturn(null) - .when(gitHubClient) - .fetchSingleEntity( - eq(GitHubQueryConstants.REPOSITORY_STATISTICS), - anyMap(), - eq(GitHubRepositoryStatisticsDTO.class)); - assertNull(gitHubClient.fetchSingleEntity( - GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class)); - } - - @Test - public void getEntities_success() throws GitHubClientException { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - LabelDTO label = new LabelDTO("id", "bug", "A bug label", "red"); - GitHubNodeDTO nodeDTO = new GitHubNodeDTO<>(label); - List> nodeList = List.of(nodeDTO); - GitHubPageInfo pageInfo = new GitHubPageInfo(false, null); - GitHubPaginationDTO dto = new GitHubPaginationDTO<>(nodeList, pageInfo); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dto)); - when(objectMapper.convertValue(nodeDTO, LabelDTO.class)).thenReturn(label); - - Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); - assertEquals(1, result.size()); - } - - @Test - public void getEntities_emptyEdges() throws GitHubClientException { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - GitHubPageInfo pageInfo = new GitHubPageInfo(false, null); - GitHubPaginationDTO dto = new GitHubPaginationDTO<>(List.of(), pageInfo); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dto)); - - Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); - assertTrue(result.isEmpty()); - } - - @Test - public void getEntities_nullEdges() throws GitHubClientException { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - GitHubPageInfo pageInfo = new GitHubPageInfo(false, null); - GitHubPaginationDTO dtoObj = new GitHubPaginationDTO<>(null, pageInfo); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); - when(objectMapper.convertValue(null, LabelDTO.class)).thenReturn(null); - - Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); - assertTrue(result.isEmpty()); - } - - @Test - public void getEntities_graphQLThrowsException() { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); - - assertThrows( - GitHubClientException.class, - () -> gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class)); - } - - @Test - public void getNodes_success() throws GitHubClientException { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - IssuePriorityDTO dto = new IssuePriorityDTO("id", "name", "color", "desc"); - GitHubPrioritySingleSelectDTO.SingleSelectObject obj = - new GitHubPrioritySingleSelectDTO.SingleSelectObject("priority", List.of(dto)); - GitHubPageInfo pageInfo = new GitHubPageInfo(false, null); - GitHubPrioritySingleSelectDTO dtoObj = new GitHubPrioritySingleSelectDTO(List.of(obj), pageInfo); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); - - Set result = gitHubClient.getNodes( - GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); - - assertEquals(1, result.size()); - } - - @Test - public void getNodes_emptyNodes() throws GitHubClientException { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - GitHubPageInfo pageInfo = new GitHubPageInfo(false, null); - GitHubPrioritySingleSelectDTO dtoObj = new GitHubPrioritySingleSelectDTO(Collections.emptyList(), pageInfo); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); - - Set result = gitHubClient.getNodes( - GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); - - assertTrue(result.isEmpty()); - } - - @Test - public void getNodes_nullResponse() throws Exception { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.empty()); - - Set result = gitHubClient.getNodes( - GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); - - assertTrue(result.isEmpty()); - } - - @Test - public void getNodes_graphQLThrowsException() { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); - - assertThrows( - GitHubClientException.class, - () -> gitHubClient.getNodes( - GitHubQueryConstants.ISSUE_PRIORITIES, - new HashMap<>(), - new ParameterizedTypeReference() {})); - } - - @Test - public void fetchSingleEntity_success() throws GitHubClientException { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - GitHubRepositoryStatisticsDTO statistics = new GitHubRepositoryStatisticsDTO( - new GitHubTotalCountDTO(10), - new GitHubTotalCountDTO(5), - new GitHubRefsDTO(List.of(new GitHubRefsDTO.GitHubBranchNodeDTO( - "main", new GitHubRefsDTO.GitHubTargetDTO(new GitHubTotalCountDTO(2)))))); - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(Class.class))).thenReturn(Mono.just(statistics)); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(statistics)); - - GitHubRepositoryStatisticsDTO result = gitHubClient.fetchSingleEntity( - GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); - - assertEquals(statistics, result); - } - - @Test - public void fetchSingleEntity_graphQLThrowsException() { - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); - - HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); - HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); - - when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); - when(reqSpec.variables(anyMap())).thenReturn(reqSpec); - when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); - when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); - - assertThrows( - GitHubClientException.class, - () -> gitHubClient.fetchSingleEntity( - GitHubQueryConstants.REPOSITORY_STATISTICS, - new HashMap<>(), - GitHubRepositoryStatisticsDTO.class)); - } -} +// package org.frankframework.insights.github; +// +// import static org.junit.jupiter.api.Assertions.*; +// import static org.mockito.Mockito.*; +// +// import com.fasterxml.jackson.databind.ObjectMapper; +// import java.util.*; +// import org.frankframework.insights.branch.BranchDTO; +// import org.frankframework.insights.common.client.graphql.GraphQLConnectionDTO; +// import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; +// import org.frankframework.insights.common.configuration.properties.GitHubProperties; +// import org.frankframework.insights.issue.IssueDTO; +// import org.frankframework.insights.issuePriority.IssuePriorityDTO; +// import org.frankframework.insights.issuetype.IssueTypeDTO; +// import org.frankframework.insights.label.LabelDTO; +// import org.frankframework.insights.milestone.MilestoneDTO; +// import org.frankframework.insights.pullrequest.PullRequestDTO; +// import org.frankframework.insights.release.ReleaseDTO; +// import org.junit.jupiter.api.BeforeEach; +// import org.junit.jupiter.api.Test; +// import org.mockito.*; +// import org.springframework.core.ParameterizedTypeReference; +// import org.springframework.graphql.client.HttpGraphQlClient; +// import reactor.core.publisher.Mono; +// +// public class GitHubClientTest { +// +// @Mock +// private ObjectMapper objectMapper; +// +// @Mock +// private GitHubProperties gitHubProperties; +// +// @Mock +// private HttpGraphQlClient httpGraphQlClient; +// +// private GitHubClient gitHubClient; +// +// private static class TestableGitHubClient extends GitHubClient { +// private final HttpGraphQlClient testClient; +// +// public TestableGitHubClient(GitHubProperties props, ObjectMapper om, HttpGraphQlClient client) { +// super(props, om); +// this.testClient = client; +// } +// +// @Override +// protected HttpGraphQlClient getGraphQlClient() { +// return testClient; +// } +// } +// +// @BeforeEach +// public void setUp() { +// MockitoAnnotations.openMocks(this); +// when(gitHubProperties.getUrl()).thenReturn("https://api.github.com"); +// when(gitHubProperties.getSecret()).thenReturn("secret"); +// } +// +// @Test +// public void getLabels_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// LabelDTO label1 = new LabelDTO("id1", "Label 1", "Label 1 description", "red"); +// LabelDTO label2 = new LabelDTO("id2", "Label 2", "Label 2 description", "blue"); +// Set labels = Set.of(label1, label2); +// doReturn(labels).when(gitHubClient).getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), +// eq(LabelDTO.class)); +// Set result = gitHubClient.getLabels(); +// assertEquals(labels, result); +// } +// +// @Test +// public void getLabels_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); +// assertTrue(gitHubClient.getLabels().isEmpty()); +// } +// +// @Test +// public void getLabels_nullReturned() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(null).when(gitHubClient).getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); +// assertThrows(NullPointerException.class, () -> gitHubClient.getLabels()); +// } +// +// @Test +// public void getLabels_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getLabels()); +// } +// +// @Test +// public void getMilestones_success() throws Exception { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// MilestoneDTO m1 = new MilestoneDTO("id", 1, null, "https//example.com", GitHubPropertyState.OPEN, null, 0, 0); +// Set milestones = Set.of(m1); +// doReturn(milestones) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); +// assertEquals(milestones, gitHubClient.getMilestones()); +// } +// +// @Test +// public void getMilestones_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); +// assertTrue(gitHubClient.getMilestones().isEmpty()); +// } +// +// @Test +// public void getMilestones_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getMilestones()); +// } +// +// @Test +// public void getIssueTypes_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// IssueTypeDTO t1 = new IssueTypeDTO("id", "Bug", "A bug in the code", "bug"); +// Set set = Set.of(t1); +// doReturn(set) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); +// assertEquals(set, gitHubClient.getIssueTypes()); +// } +// +// @Test +// public void getIssueTypes_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); +// assertTrue(gitHubClient.getIssueTypes().isEmpty()); +// } +// +// @Test +// public void getIssueTypes_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getIssueTypes()); +// } +// +// @Test +// public void getIssuePriorities_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// String projectId = "pid"; +// GitHubPrioritySingleSelectDTO.SingleSelectObject obj = +// new GitHubPrioritySingleSelectDTO.SingleSelectObject("priority", null); +// Set set = Set.of(obj); +// +// doReturn(set) +// .when(gitHubClient) +// .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); +// +// assertEquals(set, gitHubClient.getIssuePriorities(projectId)); +// } +// +// @Test +// public void getIssuePriorities_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// String projectId = "pid"; +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); +// +// assertTrue(gitHubClient.getIssuePriorities(projectId).isEmpty()); +// } +// +// @Test +// public void getIssuePriorities_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// String projectId = "pid"; +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); +// +// assertThrows(GitHubClientException.class, () -> gitHubClient.getIssuePriorities(projectId)); +// } +// +// @Test +// public void getBranches_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// BranchDTO branch = new BranchDTO("id", "main"); +// Set set = Set.of(branch); +// doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), +// eq(BranchDTO.class)); +// assertEquals(set, gitHubClient.getBranches()); +// } +// +// @Test +// public void getBranches_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); +// assertTrue(gitHubClient.getBranches().isEmpty()); +// } +// +// @Test +// public void getBranches_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getBranches()); +// } +// +// @Test +// public void getIssues_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// +// IssueDTO issue = new IssueDTO( +// "id", +// 1, +// "Test Issue", +// GitHubPropertyState.OPEN, +// null, +// "http://example.com", +// new GitHubEdgesDTO<>(null), +// null, +// null, +// new GitHubEdgesDTO<>(null), +// new GitHubEdgesDTO<>(null)); +// Set issues = Set.of(issue); +// +// doReturn(issues).when(gitHubClient).getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), +// eq(IssueDTO.class)); +// assertEquals(issues, gitHubClient.getIssues()); +// } +// +// @Test +// public void getIssues_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); +// assertTrue(gitHubClient.getIssues().isEmpty()); +// } +// +// @Test +// public void getIssues_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getIssues()); +// } +// +// @Test +// public void getBranchPullRequests_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// PullRequestDTO pr = +// new PullRequestDTO("id", 1, "Test PR", GitHubPropertyState.OPEN.name(), null, null, null, null); +// Set prs = Set.of(pr); +// doReturn(prs) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); +// assertEquals(prs, gitHubClient.getBranchPullRequests("main")); +// } +// +// @Test +// public void getBranchPullRequests_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); +// assertTrue(gitHubClient.getBranchPullRequests("main").isEmpty()); +// } +// +// @Test +// public void getBranchPullRequests_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getBranchPullRequests("main")); +// } +// +// @Test +// public void getReleases_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// ReleaseDTO dto = new ReleaseDTO("id", "v1.0", "Release 1.0", null); +// Set set = Set.of(dto); +// doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), +// eq(ReleaseDTO.class)); +// assertEquals(set, gitHubClient.getReleases()); +// } +// +// @Test +// public void getReleases_empty() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(Collections.emptySet()) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); +// assertTrue(gitHubClient.getReleases().isEmpty()); +// } +// +// @Test +// public void getReleases_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getReleases()); +// } +// +// @Test +// public void getRepositoryStatistics_success() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// GitHubRepositoryStatisticsDTO stats = new GitHubRepositoryStatisticsDTO(null, null, null); +// doReturn(stats) +// .when(gitHubClient) +// .fetchSingleEntity( +// eq(GitHubQueryConstants.REPOSITORY_STATISTICS), +// anyMap(), +// eq(GitHubRepositoryStatisticsDTO.class)); +// assertEquals(stats, gitHubClient.getRepositoryStatistics()); +// } +// +// @Test +// public void getRepositoryStatistics_exception() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doThrow(new GitHubClientException("fail", null)) +// .when(gitHubClient) +// .fetchSingleEntity( +// eq(GitHubQueryConstants.REPOSITORY_STATISTICS), +// anyMap(), +// eq(GitHubRepositoryStatisticsDTO.class)); +// assertThrows(GitHubClientException.class, () -> gitHubClient.getRepositoryStatistics()); +// } +// +// @Test +// public void getEntities_handlesNullEdgeCollection() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(null).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), +// eq(BranchDTO.class)); +// assertThrows(NullPointerException.class, () -> gitHubClient.getBranches()); +// } +// +// @Test +// public void getEntities_handlesNullQueryVariables() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// BranchDTO branch = new BranchDTO("id", "main"); +// Set set = Set.of(branch); +// doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), isNull(), +// eq(BranchDTO.class)); +// } +// +// @Test +// public void getNodes_handlesNullResult() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(null).when(gitHubClient).getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any()); +// assertThrows(NullPointerException.class, () -> gitHubClient.getIssuePriorities("pid")); +// } +// +// @Test +// public void fetchSingleEntity_nullResponse() throws GitHubClientException { +// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); +// doReturn(null) +// .when(gitHubClient) +// .fetchSingleEntity( +// eq(GitHubQueryConstants.REPOSITORY_STATISTICS), +// anyMap(), +// eq(GitHubRepositoryStatisticsDTO.class)); +// assertNull(gitHubClient.fetchSingleEntity( +// GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class)); +// } +// +// @Test +// public void getEntities_success() throws GitHubClientException { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// LabelDTO label = new LabelDTO("id", "bug", "A bug label", "red"); +// GraphQLNodeDTO nodeDTO = new GraphQLNodeDTO<>(label); +// List> nodeList = List.of(nodeDTO); +// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); +// GraphQLConnectionDTO dto = new GraphQLConnectionDTO<>(nodeList, pageInfo); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dto)); +// when(objectMapper.convertValue(nodeDTO, LabelDTO.class)).thenReturn(label); +// +// Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); +// assertEquals(1, result.size()); +// } +// +// @Test +// public void getEntities_emptyEdges() throws GitHubClientException { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); +// GraphQLConnectionDTO dto = new GraphQLConnectionDTO<>(List.of(), pageInfo); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dto)); +// +// Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); +// assertTrue(result.isEmpty()); +// } +// +// @Test +// public void getEntities_nullEdges() throws GitHubClientException { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); +// GraphQLConnectionDTO dtoObj = new GraphQLConnectionDTO<>(null, pageInfo); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); +// when(objectMapper.convertValue(null, LabelDTO.class)).thenReturn(null); +// +// Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); +// assertTrue(result.isEmpty()); +// } +// +// @Test +// public void getEntities_graphQLThrowsException() { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); +// +// assertThrows( +// GitHubClientException.class, +// () -> gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class)); +// } +// +// @Test +// public void getNodes_success() throws GitHubClientException { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// IssuePriorityDTO dto = new IssuePriorityDTO("id", "name", "color", "desc"); +// GitHubPrioritySingleSelectDTO.SingleSelectObject obj = +// new GitHubPrioritySingleSelectDTO.SingleSelectObject("priority", List.of(dto)); +// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); +// GitHubPrioritySingleSelectDTO dtoObj = new GitHubPrioritySingleSelectDTO(List.of(obj), pageInfo); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); +// +// Set result = gitHubClient.getNodes( +// GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); +// +// assertEquals(1, result.size()); +// } +// +// @Test +// public void getNodes_emptyNodes() throws GitHubClientException { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); +// GitHubPrioritySingleSelectDTO dtoObj = new GitHubPrioritySingleSelectDTO(Collections.emptyList(), pageInfo); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); +// +// Set result = gitHubClient.getNodes( +// GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); +// +// assertTrue(result.isEmpty()); +// } +// +// @Test +// public void getNodes_nullResponse() throws Exception { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.empty()); +// +// Set result = gitHubClient.getNodes( +// GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); +// +// assertTrue(result.isEmpty()); +// } +// +// @Test +// public void getNodes_graphQLThrowsException() { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); +// +// assertThrows( +// GitHubClientException.class, +// () -> gitHubClient.getNodes( +// GitHubQueryConstants.ISSUE_PRIORITIES, +// new HashMap<>(), +// new ParameterizedTypeReference() {})); +// } +// +// @Test +// public void fetchSingleEntity_success() throws GitHubClientException { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// GitHubRepositoryStatisticsDTO statistics = new GitHubRepositoryStatisticsDTO( +// new GitHubTotalCountDTO(10), +// new GitHubTotalCountDTO(5), +// new GitHubRefsDTO(List.of(new GitHubRefsDTO.GitHubBranchNodeDTO( +// "main", new GitHubRefsDTO.GitHubTargetDTO(new GitHubTotalCountDTO(2)))))); +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(Class.class))).thenReturn(Mono.just(statistics)); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(statistics)); +// +// GitHubRepositoryStatisticsDTO result = gitHubClient.fetchSingleEntity( +// GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); +// +// assertEquals(statistics, result); +// } +// +// @Test +// public void fetchSingleEntity_graphQLThrowsException() { +// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); +// +// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); +// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); +// +// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); +// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); +// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); +// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); +// +// assertThrows( +// GitHubClientException.class, +// () -> gitHubClient.fetchSingleEntity( +// GitHubQueryConstants.REPOSITORY_STATISTICS, +// new HashMap<>(), +// GitHubRepositoryStatisticsDTO.class)); +// } +// } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/issue/IssueServiceTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/issue/IssueServiceTest.java index e419a749..55a487c2 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/issue/IssueServiceTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/issue/IssueServiceTest.java @@ -5,6 +5,7 @@ import java.time.OffsetDateTime; import java.util.*; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; import org.frankframework.insights.common.entityconnection.issuelabel.IssueLabel; import org.frankframework.insights.common.entityconnection.issuelabel.IssueLabelRepository; import org.frankframework.insights.common.mapper.Mapper; @@ -118,8 +119,8 @@ public void setup() { LabelDTO labelDTO = new LabelDTO("l1", "bug", "desc", "red"); - GitHubNodeDTO labelNode = new GitHubNodeDTO<>(labelDTO); - List> labelNodeList = List.of(labelNode); + GraphQLNodeDTO labelNode = new GraphQLNodeDTO<>(labelDTO); + List> labelNodeList = List.of(labelNode); GitHubEdgesDTO labelEdges = new GitHubEdgesDTO<>(labelNodeList); GitHubEdgesDTO emptyProjectItems = new GitHubEdgesDTO<>(Collections.emptyList()); @@ -150,7 +151,7 @@ public void setup() { null, emptyProjectItems); - GitHubNodeDTO subIssueNode = new GitHubNodeDTO<>(dto2); + GraphQLNodeDTO subIssueNode = new GraphQLNodeDTO<>(dto2); GitHubEdgesDTO subIssuesEdge = new GitHubEdgesDTO<>(List.of(subIssueNode)); dtoSub = new IssueDTO( diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/pullrequest/PullRequestServiceTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/pullrequest/PullRequestServiceTest.java index 1b0a3e25..dacf2694 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/pullrequest/PullRequestServiceTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/pullrequest/PullRequestServiceTest.java @@ -9,6 +9,7 @@ import java.util.*; import org.frankframework.insights.branch.Branch; import org.frankframework.insights.branch.BranchService; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; import org.frankframework.insights.common.configuration.properties.GitHubProperties; import org.frankframework.insights.common.entityconnection.branchpullrequest.BranchPullRequest; import org.frankframework.insights.common.entityconnection.branchpullrequest.BranchPullRequestRepository; @@ -19,7 +20,6 @@ import org.frankframework.insights.github.GitHubClient; import org.frankframework.insights.github.GitHubClientException; import org.frankframework.insights.github.GitHubEdgesDTO; -import org.frankframework.insights.github.GitHubNodeDTO; import org.frankframework.insights.github.GitHubPropertyState; import org.frankframework.insights.issue.Issue; import org.frankframework.insights.issue.IssueDTO; @@ -248,13 +248,13 @@ public void injectBranchPullRequests_shouldSaveLabelsAndIssues() LabelDTO labelDTO = new LabelDTO("l1", "bug", "desc", "red"); - GitHubNodeDTO labelNode = new GitHubNodeDTO<>(labelDTO); - List> labelNodeList = List.of(labelNode); + GraphQLNodeDTO labelNode = new GraphQLNodeDTO<>(labelDTO); + List> labelNodeList = List.of(labelNode); GitHubEdgesDTO labelEdges = new GitHubEdgesDTO<>(labelNodeList); IssueDTO issue = new IssueDTO("i1", 1, "issue1", GitHubPropertyState.OPEN, null, null, null, null, null, null, null); - GitHubNodeDTO issueNode = new GitHubNodeDTO<>(issue); + GraphQLNodeDTO issueNode = new GraphQLNodeDTO<>(issue); GitHubEdgesDTO closingIssuesEdges = new GitHubEdgesDTO<>(List.of(issueNode)); PullRequestDTO prDto = new PullRequestDTO( diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java index 053c362e..571ec34d 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java @@ -6,8 +6,9 @@ import javax.sql.DataSource; import net.javacrumbs.shedlock.core.LockAssert; import org.frankframework.insights.branch.BranchService; -import org.frankframework.insights.common.configuration.SystemDataInitializer; -import org.frankframework.insights.common.configuration.properties.GitHubProperties; +import org.frankframework.insights.common.configuration.GitHubConfiguration; +import org.frankframework.insights.common.configuration.SnykConfiguration; +import org.frankframework.insights.common.configuration.properties.FetchProperties; import org.frankframework.insights.github.GitHubRepositoryStatisticsService; import org.frankframework.insights.issue.IssueService; import org.frankframework.insights.issuePriority.IssuePriorityService; @@ -57,15 +58,19 @@ public class ShedLockLocalTest { private ReleaseService releaseService; @Mock - private GitHubProperties gitHubProperties; + private FetchProperties fetchProperties; - private SystemDataInitializer systemDataInitializer; + private GitHubConfiguration gitHubConfiguration; + + private SnykConfiguration snykConfiguration; + + // todo expand test classes with snyk client @BeforeEach public void setUp() { - when(gitHubProperties.getFetch()).thenReturn(false); + when(fetchProperties.getEnabled()).thenReturn(false); - systemDataInitializer = new SystemDataInitializer( + gitHubConfiguration = new GitHubConfiguration( gitHubRepositoryStatisticsService, labelService, milestoneService, @@ -75,14 +80,18 @@ public void setUp() { issueService, pullRequestService, releaseService, - gitHubProperties); + fetchProperties); + + snykConfiguration = new SnykConfiguration( + fetchProperties + ); LockAssert.TestHelper.makeAllAssertsPass(true); } @Test public void should_SkipGitHubFetch_when_LocalProfileIsActive() { - systemDataInitializer.run(); + gitHubConfiguration.run(); verifyNoInteractions(gitHubRepositoryStatisticsService); verifyNoInteractions(labelService); @@ -94,4 +103,12 @@ public void should_SkipGitHubFetch_when_LocalProfileIsActive() { verifyNoInteractions(pullRequestService); verifyNoInteractions(releaseService); } + + @Test + public void should_SkipSnykFetch_when_LocalProfileIsActive() { + snykConfiguration.run(); + + //todo add services here +// verifyNoInteractions(vulnerabilityService); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java index 4a1805c2..4d4f8b3c 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java @@ -8,8 +8,9 @@ import net.javacrumbs.shedlock.core.LockAssert; import org.frankframework.insights.branch.BranchInjectionException; import org.frankframework.insights.branch.BranchService; -import org.frankframework.insights.common.configuration.SystemDataInitializer; -import org.frankframework.insights.common.configuration.properties.GitHubProperties; +import org.frankframework.insights.common.configuration.GitHubConfiguration; +import org.frankframework.insights.common.configuration.SnykConfiguration; +import org.frankframework.insights.common.configuration.properties.FetchProperties; import org.frankframework.insights.github.GitHubClientException; import org.frankframework.insights.github.GitHubRepositoryStatisticsService; import org.frankframework.insights.issue.IssueInjectionException; @@ -67,15 +68,17 @@ public class ShedLockProductionTest { private ReleaseService releaseService; @Mock - private GitHubProperties gitHubProperties; + private FetchProperties fetchProperties; - private SystemDataInitializer systemDataInitializer; + private GitHubConfiguration gitHubConfiguration; + + private SnykConfiguration snykConfiguration; @BeforeEach public void setUp() { - when(gitHubProperties.getFetch()).thenReturn(true); + when(fetchProperties.getEnabled()).thenReturn(true); - systemDataInitializer = new SystemDataInitializer( + gitHubConfiguration = new GitHubConfiguration( gitHubRepositoryStatisticsService, labelService, milestoneService, @@ -85,7 +88,11 @@ public void setUp() { issueService, pullRequestService, releaseService, - gitHubProperties); + fetchProperties); + + snykConfiguration = new SnykConfiguration( + fetchProperties + ); LockAssert.TestHelper.makeAllAssertsPass(true); } @@ -95,7 +102,7 @@ public void should_FetchGitHubData_when_ProductionProfileIsActive() throws LabelInjectionException, GitHubClientException, MilestoneInjectionException, BranchInjectionException, ReleaseInjectionException, IssueInjectionException, PullRequestInjectionException, IssueTypeInjectionException, IssuePriorityInjectionException { - systemDataInitializer.run(); + gitHubConfiguration.run(); verify(gitHubRepositoryStatisticsService, times(1)).fetchRepositoryStatistics(); verify(labelService, times(1)).injectLabels(); @@ -107,4 +114,11 @@ public void should_FetchGitHubData_when_ProductionProfileIsActive() verify(pullRequestService, times(1)).injectBranchPullRequests(); verify(releaseService, times(1)).injectReleases(); } + + @Test + public void should_FetchSnykData_when_ProductionProfileIsActive() { + snykConfiguration.run(); + + verify(vulnerabilityService, times(1)).injectVulnerabilities(); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java index 2516c692..6c60f99e 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java @@ -11,9 +11,9 @@ import net.javacrumbs.shedlock.core.LockAssert; import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; import org.frankframework.insights.branch.BranchService; +import org.frankframework.insights.common.configuration.GitHubConfiguration; import org.frankframework.insights.common.configuration.ShedLockConfiguration; -import org.frankframework.insights.common.configuration.SystemDataInitializer; -import org.frankframework.insights.common.configuration.properties.GitHubProperties; +import org.frankframework.insights.common.configuration.properties.FetchProperties; import org.frankframework.insights.github.GitHubRepositoryStatisticsService; import org.frankframework.insights.issue.IssueService; import org.frankframework.insights.issuePriority.IssuePriorityService; @@ -62,13 +62,13 @@ public class ShedLockTest { private ReleaseService releaseService; @Mock - private GitHubProperties gitHubProperties; + private FetchProperties fetchProperties; - private SystemDataInitializer systemDataInitializer; + private GitHubConfiguration gitHubConfiguration; @BeforeEach public void setUp() { - systemDataInitializer = new SystemDataInitializer( + gitHubConfiguration = new GitHubConfiguration( gitHubRepositoryStatisticsService, labelService, milestoneService, @@ -78,7 +78,7 @@ public void setUp() { issueService, pullRequestService, releaseService, - gitHubProperties); + fetchProperties); LockAssert.TestHelper.makeAllAssertsPass(true); } @@ -93,13 +93,13 @@ public void should_CreateLockProvider_when_BeanIsInitialized() { @Test public void should_LockStartupTask_when_Executed() { - systemDataInitializer.run(); + gitHubConfiguration.run(); LockAssert.assertLocked(); } @Test public void should_LockDailyJob_when_Executed() { - systemDataInitializer.dailyJob(); + gitHubConfiguration.dailyJob(); LockAssert.assertLocked(); } @@ -110,13 +110,13 @@ public void should_NotAllowStartupTaskToInterrupt_when_DailyJobIsRunning() throw Future startupFuture = executorService.submit(() -> { latch.countDown(); - systemDataInitializer.run(); + gitHubConfiguration.run(); }); Future dailyJobFuture = executorService.submit(() -> { try { latch.await(); - systemDataInitializer.dailyJob(); + gitHubConfiguration.dailyJob(); } catch (InterruptedException e) { throw new RuntimeException("Thread was interrupted while waiting", e); } @@ -143,7 +143,7 @@ public void should_NotAllowDailyJobToInterrupt_when_StartupTaskIsRunning() throw Future dailyJobFuture = executorService.submit(() -> { try { latch.await(); - systemDataInitializer.dailyJob(); + gitHubConfiguration.dailyJob(); } catch (InterruptedException e) { throw new RuntimeException("Thread was interrupted while waiting", e); } @@ -151,7 +151,7 @@ public void should_NotAllowDailyJobToInterrupt_when_StartupTaskIsRunning() throw Future startupFuture = executorService.submit(() -> { latch.countDown(); - systemDataInitializer.run(); + gitHubConfiguration.run(); }); dailyJobFuture.get(); From cfac26e618b25ba45da4165b176d06476753c956 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 4 Sep 2025 10:08:01 +0200 Subject: [PATCH 2/5] refactored all tests and removed redundant tests --- .../common/client/graphql/GraphQLClient.java | 198 ++--- .../insights/github/GitHubClient.java | 397 +++++----- .../insights/client/ApiClientTest.java | 4 - .../insights/client/GraphQLClientTest.java | 4 - .../insights/client/RestClientTest.java | 4 - .../insights/common/client/ApiClientTest.java | 65 ++ .../client/graphql/GraphQLClientTest.java | 199 +++++ .../common/client/rest/RestClientTest.java | 133 ++++ .../insights/github/GitHubClientTest.java | 747 ++++-------------- .../shedlock/ShedLockProductionTest.java | 2 +- .../insights/shedlock/ShedLockTest.java | 3 + 11 files changed, 859 insertions(+), 897 deletions(-) delete mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java delete mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java delete mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java create mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java create mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java create mode 100644 InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java index fdea12a7..05c887eb 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java @@ -16,115 +16,115 @@ @Slf4j public abstract class GraphQLClient extends ApiClient { - private final HttpGraphQlClient graphQlClient; - private final ObjectMapper objectMapper; + private final HttpGraphQlClient graphQlClient; + private final ObjectMapper objectMapper; - public GraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { - super(baseUrl, configurer); - this.graphQlClient = HttpGraphQlClient.builder(this.webClient).build(); - this.objectMapper = objectMapper; - } + public GraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { + super(baseUrl, configurer); + this.graphQlClient = HttpGraphQlClient.builder(this.webClient).build(); + this.objectMapper = objectMapper; + } - protected T fetchSingleEntity(GraphQLQuery query, Map queryVariables, Class entityType) - throws GraphQLClientException { - try { - return getGraphQlClient() - .documentName(query.getDocumentName()) - .variables(queryVariables) - .retrieve(query.getRetrievePath()) - .toEntity(entityType) - .block(); - } catch (Exception e) { - throw new GraphQLClientException("Failed GraphQL request for document: " + query.getDocumentName(), e); - } - } + protected T fetchSingleEntity(GraphQLQuery query, Map queryVariables, Class entityType) + throws GraphQLClientException { + try { + return getGraphQlClient() + .documentName(query.getDocumentName()) + .variables(queryVariables) + .retrieve(query.getRetrievePath()) + .toEntity(entityType) + .block(); + } catch (Exception e) { + throw new GraphQLClientException("Failed GraphQL request for document: " + query.getDocumentName(), e); + } + } - /** - * A flexible method to fetch paginated data, allowing the caller to define how collections are extracted. - * This can handle various GraphQL response structures, such as those with 'edges' or 'nodes'. - * - * @param query the GraphQL query constant. - * @param queryVariables the variables for the query. - * @param entityType the class type of the final entities. - * @param responseType the type reference for deserializing the raw GraphQL response. - * @param collectionExtractor a function to extract the collection of raw data maps from the response. - * @param pageInfoExtractor a function to extract pagination info from the response. - * @return a set of all fetched entities across all pages. - * @throws GraphQLClientException if the request fails. - */ - protected Set fetchPaginatedCollection( - GraphQLQuery query, - Map queryVariables, - Class entityType, - ParameterizedTypeReference responseType, - Function>> collectionExtractor, - Function pageInfoExtractor) - throws GraphQLClientException { + /** + * A flexible method to fetch paginated data, allowing the caller to define how collections are extracted. + * This can handle various GraphQL response structures, such as those with 'edges' or 'nodes'. + * + * @param query the GraphQL query constant. + * @param queryVariables the variables for the query. + * @param entityType the class type of the final entities. + * @param responseType the type reference for deserializing the raw GraphQL response. + * @param collectionExtractor a function to extract the collection of raw data maps from the response. + * @param pageInfoExtractor a function to extract pagination info from the response. + * @return a set of all fetched entities across all pages. + * @throws GraphQLClientException if the request fails. + */ + protected Set fetchPaginatedCollection( + GraphQLQuery query, + Map queryVariables, + Class entityType, + ParameterizedTypeReference responseType, + Function>> collectionExtractor, + Function pageInfoExtractor) + throws GraphQLClientException { - Function> entityExtractor = response -> { - Collection> rawNodes = collectionExtractor.apply(response); - if (rawNodes == null) { - return Set.of(); - } - return rawNodes.stream() - .map(node -> objectMapper.convertValue(node, entityType)) - .collect(Collectors.toList()); - }; + Function> entityExtractor = response -> { + Collection> rawNodes = collectionExtractor.apply(response); + if (rawNodes == null) { + return Set.of(); + } + return rawNodes.stream() + .map(node -> objectMapper.convertValue(node, entityType)) + .collect(Collectors.toList()); + }; - return fetchPaginated(query, queryVariables, responseType, entityExtractor, pageInfoExtractor); - } + return fetchPaginated(query, queryVariables, responseType, entityExtractor, pageInfoExtractor); + } - private Set fetchPaginated( - GraphQLQuery query, - Map queryVariables, - ParameterizedTypeReference responseType, - Function> entityExtractor, - Function pageInfoExtractor) - throws GraphQLClientException { - try { - Set allEntities = new HashSet<>(); - String cursor = null; - boolean hasNextPage = true; + private Set fetchPaginated( + GraphQLQuery query, + Map queryVariables, + ParameterizedTypeReference responseType, + Function> entityExtractor, + Function pageInfoExtractor) + throws GraphQLClientException { + try { + Set allEntities = new HashSet<>(); + String cursor = null; + boolean hasNextPage = true; - while (hasNextPage) { - queryVariables.put("after", cursor); - RAW response = getGraphQlClient() - .documentName(query.getDocumentName()) - .variables(queryVariables) - .retrieve(query.getRetrievePath()) - .toEntity(responseType) - .block(); + while (hasNextPage) { + queryVariables.put("after", cursor); + RAW response = getGraphQlClient() + .documentName(query.getDocumentName()) + .variables(queryVariables) + .retrieve(query.getRetrievePath()) + .toEntity(responseType) + .block(); - if (response == null) { - log.warn("Received null response for query: {}", query); - break; - } + if (response == null) { + log.warn("Received null response for query: {}", query); + break; + } - Collection entities = entityExtractor.apply(response); - if (entities == null || entities.isEmpty()) { - log.warn("Received empty entities for query: {}", query); - break; - } + Collection entities = entityExtractor.apply(response); + if (entities == null || entities.isEmpty()) { + log.warn("Received empty entities for query: {}", query); + break; + } - allEntities.addAll(entities); - log.info("Fetched {} entities with query: {}", entities.size(), query); + allEntities.addAll(entities); + log.info("Fetched {} entities with query: {}", entities.size(), query); - GraphQLPageInfoDTO pageInfo = pageInfoExtractor.apply(response); - hasNextPage = pageInfo != null && pageInfo.hasNextPage(); - cursor = (pageInfo != null) ? pageInfo.endCursor() : null; - } - log.info( - "Completed paginated fetch for [{}], total entities found: {}", - query.getDocumentName(), - allEntities.size()); - return allEntities; - } catch (Exception e) { - throw new GraphQLClientException( - "Failed paginated GraphQL request for document: " + query.getDocumentName(), e); - } - } + GraphQLPageInfoDTO pageInfo = pageInfoExtractor.apply(response); + hasNextPage = pageInfo != null && pageInfo.hasNextPage(); + cursor = (pageInfo != null) ? pageInfo.endCursor() : null; + } + log.info( + "Completed paginated fetch for [{}], total entities found: {}", + query.getDocumentName(), + allEntities.size()); + return allEntities; + } catch (Exception e) { + throw new GraphQLClientException( + "Failed paginated GraphQL request for document: " + query.getDocumentName(), e); + } + } - private HttpGraphQlClient getGraphQlClient() { - return graphQlClient; - } + protected HttpGraphQlClient getGraphQlClient() { + return graphQlClient; + } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java index ae4e433f..2cea465e 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java @@ -27,203 +27,202 @@ @Component @Slf4j -public class GitHubClient extends GraphQLClient { - - public GitHubClient(GitHubProperties gitHubProperties, ObjectMapper objectMapper) { - super( - gitHubProperties.getUrl(), - builder -> { - if (gitHubProperties.getSecret() != null - && !gitHubProperties.getSecret().isEmpty()) { - builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + gitHubProperties.getSecret()); - } - }, - objectMapper); - } - - /** - * Fetches repository statistics from GitHub. - * @return GitHubRepositoryStatisticsDTO containing repository statistics - * @throws GitHubClientException if an error occurs during the request - */ - public GitHubRepositoryStatisticsDTO getRepositoryStatistics() throws GitHubClientException { - try { - GitHubRepositoryStatisticsDTO repositoryStatisticsDTO = fetchSingleEntity( - GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); - log.info("Fetched repository statistics from GitHub"); - return repositoryStatisticsDTO; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch repository statistics from GitHub.", e); - } - } - - /** - * Fetches labels from GitHub. - * @return Set of LabelDTO containing labels - * @throws GitHubClientException if an error occurs during the request - */ - public Set getLabels() throws GitHubClientException { - try { - Set labels = fetchPaginatedViaRelay(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); - log.info("Successfully fetched {} labels from GitHub", labels.size()); - return labels; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch labels from GitHub.", e); - } - } - - /** - * Fetches milestones from GitHub. - * @return Set of MilestoneDTO containing milestones - * @throws GitHubClientException if an error occurs during the request - */ - public Set getMilestones() throws GitHubClientException { - try { - Set milestones = - fetchPaginatedViaRelay(GitHubQueryConstants.MILESTONES, new HashMap<>(), MilestoneDTO.class); - log.info("Successfully fetched {} milestones from GitHub", milestones.size()); - return milestones; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch milestones from GitHub.", e); - } - } - - /** - * Fetches issue types from GitHub - * @return Set of IssueTypeDTO containing issue types - * @throws GitHubClientException of an error occurs during the request - */ - public Set getIssueTypes() throws GitHubClientException { - try { - Set issueTypes = - fetchPaginatedViaRelay(GitHubQueryConstants.ISSUE_TYPES, new HashMap<>(), IssueTypeDTO.class); - log.info("Successfully fetched {} issueTypes from GitHub", issueTypes.size()); - return issueTypes; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch issue types from GitHub.", e); - } - } - - /** - * Fetches issue priorities from GitHub. - * @param projectId the ID of the project for which to fetch issue priorities - * @return Set of GitHubSingleSelectDTO containing issue priorities - * @throws GitHubClientException if an error occurs during the request - */ - public Set getIssuePriorities(String projectId) - throws GitHubClientException { - try { - Map variables = new HashMap<>(); - variables.put("projectId", projectId); - return fetchPaginatedViaNodes( - GitHubQueryConstants.ISSUE_PRIORITIES, - variables, - GitHubPrioritySingleSelectDTO.SingleSelectObject.class); - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch issue priorities from GitHub.", e); - } - } - - /** - * Fetches branches from GitHub. - * @return Set of BranchDTO containing branches - * @throws GitHubClientException if an error occurs during the request - */ - public Set getBranches() throws GitHubClientException { - try { - Set branches = - fetchPaginatedViaRelay(GitHubQueryConstants.BRANCHES, new HashMap<>(), BranchDTO.class); - log.info("Successfully fetched {} branches from GitHub", branches.size()); - return branches; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch branches from GitHub.", e); - } - } - - /** - * Fetches issues from GitHub. - * @return Set of IssueDTO containing issues - * @throws GitHubClientException if an error occurs during the request - */ - public Set getIssues() throws GitHubClientException { - try { - Set issues = fetchPaginatedViaRelay(GitHubQueryConstants.ISSUES, new HashMap<>(), IssueDTO.class); - log.info("Successfully fetched {} issues from GitHub", issues.size()); - return issues; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch issues from GitHub.", e); - } - } - - /** - * Fetches pull requests for a specific branch from GitHub. - * @param branchName the name of the branch - * @return Set of PullRequestDTO containing pull requests for the specified branch - * @throws GitHubClientException if an error occurs during the request - */ - public Set getBranchPullRequests(String branchName) throws GitHubClientException { - try { - Map variables = new HashMap<>(); - variables.put("branchName", branchName); - Set pullRequests = - fetchPaginatedViaRelay(GitHubQueryConstants.BRANCH_PULLS, variables, PullRequestDTO.class); - log.info( - "Successfully fetched {} pull requests for branch {} from GitHub", pullRequests.size(), branchName); - return pullRequests; - } catch (GraphQLClientException e) { - throw new GitHubClientException( - String.format("Failed to fetch pull requests for branch '%s' from GitHub.", branchName), e); - } - } - - public Set getReleases() throws GitHubClientException { - try { - Set releases = - fetchPaginatedViaRelay(GitHubQueryConstants.RELEASES, new HashMap<>(), ReleaseDTO.class); - log.info("Successfully fetched {} releases from GitHub", releases.size()); - return releases; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch releases from GitHub.", e); - } - } - - /** - * Helper to fetch paginated data from a standard Relay-style GraphQL connection. - * @param query the GraphQL query to execute - * @param queryVariables the variables for the GraphQL query - * @param entityType the class type of the entities to fetch - * @return Set of entities of type T - * @param the type of entities to fetch - * @throws GraphQLClientException if an error occurs during the request - */ - private Set fetchPaginatedViaRelay( - GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - - Function>, Collection>> collectionExtractor = - connection -> connection.edges() == null - ? Set.of() - : connection.edges().stream().map(GraphQLNodeDTO::node).collect(Collectors.toList()); - - return fetchPaginatedCollection( - query, queryVariables, entityType, responseType, collectionExtractor, GraphQLConnectionDTO::pageInfo); - } - - /** - * Helper to fetch paginated data from a GraphQL connection using the 'nodes' field. - * @param query the GraphQL query to execute - * @param queryVariables the variables for the GraphQL query - * @param entityType the class type of the entities to fetch - * @return Set of entities of type T - * @param the type of entities to fetch - * @throws GraphQLClientException if an error occurs during the request - */ - private Set fetchPaginatedViaNodes( - GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - return fetchPaginatedCollection( - query, queryVariables, entityType, responseType, GitHubNodesDTO::nodes, GitHubNodesDTO::pageInfo); - } +public abstract class GitHubClient extends GraphQLClient { + public GitHubClient(GitHubProperties gitHubProperties, ObjectMapper objectMapper) { + super( + gitHubProperties.getUrl(), + builder -> { + if (gitHubProperties.getSecret() != null + && !gitHubProperties.getSecret().isEmpty()) { + builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + gitHubProperties.getSecret()); + } + }, + objectMapper); + } + + /** + * Fetches repository statistics from GitHub. + * @return GitHubRepositoryStatisticsDTO containing repository statistics + * @throws GitHubClientException if an error occurs during the request + */ + public GitHubRepositoryStatisticsDTO getRepositoryStatistics() throws GitHubClientException { + try { + GitHubRepositoryStatisticsDTO repositoryStatisticsDTO = fetchSingleEntity( + GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); + log.info("Fetched repository statistics from GitHub"); + return repositoryStatisticsDTO; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch repository statistics from GitHub.", e); + } + } + + /** + * Fetches labels from GitHub. + * @return Set of LabelDTO containing labels + * @throws GitHubClientException if an error occurs during the request + */ + public Set getLabels() throws GitHubClientException { + try { + Set labels = fetchPaginatedViaRelay(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); + log.info("Successfully fetched {} labels from GitHub", labels.size()); + return labels; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch labels from GitHub.", e); + } + } + + /** + * Fetches milestones from GitHub. + * @return Set of MilestoneDTO containing milestones + * @throws GitHubClientException if an error occurs during the request + */ + public Set getMilestones() throws GitHubClientException { + try { + Set milestones = + fetchPaginatedViaRelay(GitHubQueryConstants.MILESTONES, new HashMap<>(), MilestoneDTO.class); + log.info("Successfully fetched {} milestones from GitHub", milestones.size()); + return milestones; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch milestones from GitHub.", e); + } + } + + /** + * Fetches issue types from GitHub + * @return Set of IssueTypeDTO containing issue types + * @throws GitHubClientException of an error occurs during the request + */ + public Set getIssueTypes() throws GitHubClientException { + try { + Set issueTypes = + fetchPaginatedViaRelay(GitHubQueryConstants.ISSUE_TYPES, new HashMap<>(), IssueTypeDTO.class); + log.info("Successfully fetched {} issueTypes from GitHub", issueTypes.size()); + return issueTypes; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issue types from GitHub.", e); + } + } + + /** + * Fetches issue priorities from GitHub. + * @param projectId the ID of the project for which to fetch issue priorities + * @return Set of GitHubSingleSelectDTO containing issue priorities + * @throws GitHubClientException if an error occurs during the request + */ + public Set getIssuePriorities(String projectId) + throws GitHubClientException { + try { + Map variables = new HashMap<>(); + variables.put("projectId", projectId); + return fetchPaginatedViaNodes( + GitHubQueryConstants.ISSUE_PRIORITIES, + variables, + GitHubPrioritySingleSelectDTO.SingleSelectObject.class); + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issue priorities from GitHub.", e); + } + } + + /** + * Fetches branches from GitHub. + * @return Set of BranchDTO containing branches + * @throws GitHubClientException if an error occurs during the request + */ + public Set getBranches() throws GitHubClientException { + try { + Set branches = + fetchPaginatedViaRelay(GitHubQueryConstants.BRANCHES, new HashMap<>(), BranchDTO.class); + log.info("Successfully fetched {} branches from GitHub", branches.size()); + return branches; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch branches from GitHub.", e); + } + } + + /** + * Fetches issues from GitHub. + * @return Set of IssueDTO containing issues + * @throws GitHubClientException if an error occurs during the request + */ + public Set getIssues() throws GitHubClientException { + try { + Set issues = fetchPaginatedViaRelay(GitHubQueryConstants.ISSUES, new HashMap<>(), IssueDTO.class); + log.info("Successfully fetched {} issues from GitHub", issues.size()); + return issues; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issues from GitHub.", e); + } + } + + /** + * Fetches pull requests for a specific branch from GitHub. + * @param branchName the name of the branch + * @return Set of PullRequestDTO containing pull requests for the specified branch + * @throws GitHubClientException if an error occurs during the request + */ + public Set getBranchPullRequests(String branchName) throws GitHubClientException { + try { + Map variables = new HashMap<>(); + variables.put("branchName", branchName); + Set pullRequests = + fetchPaginatedViaRelay(GitHubQueryConstants.BRANCH_PULLS, variables, PullRequestDTO.class); + log.info( + "Successfully fetched {} pull requests for branch {} from GitHub", pullRequests.size(), branchName); + return pullRequests; + } catch (GraphQLClientException e) { + throw new GitHubClientException( + String.format("Failed to fetch pull requests for branch '%s' from GitHub.", branchName), e); + } + } + + public Set getReleases() throws GitHubClientException { + try { + Set releases = + fetchPaginatedViaRelay(GitHubQueryConstants.RELEASES, new HashMap<>(), ReleaseDTO.class); + log.info("Successfully fetched {} releases from GitHub", releases.size()); + return releases; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch releases from GitHub.", e); + } + } + + /** + * Helper to fetch paginated data from a standard Relay-style GraphQL connection. + * @param query the GraphQL query to execute + * @param queryVariables the variables for the GraphQL query + * @param entityType the class type of the entities to fetch + * @return Set of entities of type T + * @param the type of entities to fetch + * @throws GraphQLClientException if an error occurs during the request + */ + private Set fetchPaginatedViaRelay( + GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + + Function>, Collection>> collectionExtractor = + connection -> connection.edges() == null + ? Set.of() + : connection.edges().stream().map(GraphQLNodeDTO::node).collect(Collectors.toList()); + + return fetchPaginatedCollection( + query, queryVariables, entityType, responseType, collectionExtractor, GraphQLConnectionDTO::pageInfo); + } + + /** + * Helper to fetch paginated data from a GraphQL connection using the 'nodes' field. + * @param query the GraphQL query to execute + * @param queryVariables the variables for the GraphQL query + * @param entityType the class type of the entities to fetch + * @return Set of entities of type T + * @param the type of entities to fetch + * @throws GraphQLClientException if an error occurs during the request + */ + private Set fetchPaginatedViaNodes( + GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + return fetchPaginatedCollection( + query, queryVariables, entityType, responseType, GitHubNodesDTO::nodes, GitHubNodesDTO::pageInfo); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java deleted file mode 100644 index f05b6a89..00000000 --- a/InsightsBackend/src/test/java/org/frankframework/insights/client/ApiClientTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.frankframework.insights.client; - -public class ApiClientTest { -} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java deleted file mode 100644 index c2fa955c..00000000 --- a/InsightsBackend/src/test/java/org/frankframework/insights/client/GraphQLClientTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.frankframework.insights.client; - -public class GraphQLClientTest { -} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java deleted file mode 100644 index 345fb565..00000000 --- a/InsightsBackend/src/test/java/org/frankframework/insights/client/RestClientTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.frankframework.insights.client; - -public class RestClientTest { -} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java new file mode 100644 index 00000000..4b030943 --- /dev/null +++ b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java @@ -0,0 +1,65 @@ +package org.frankframework.insights.common.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.function.client.WebClient; + +@ExtendWith(MockitoExtension.class) +public class ApiClientTest { + private static class TestApiClient extends ApiClient { + TestApiClient(String baseUrl, Consumer configurer) { + super(baseUrl, configurer); + } + } + + @Test + public void constructor_withValidBaseUrl_initializesClient() { + String baseUrl = "https://api.example.com"; + + TestApiClient client = new TestApiClient(baseUrl, null); + + assertThat(client.webClient).isNotNull(); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + public void constructor_withInvalidBaseUrl_throwsIllegalArgumentException(String baseUrl) { + assertThatThrownBy(() -> new TestApiClient(baseUrl, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Base URL cannot be null or empty."); + } + + @Test + public void constructor_withConfigurer_appliesConfiguration() { + String baseUrl = "https://api.example.com"; + Consumer configurer = b -> b.defaultHeader(HttpHeaders.AUTHORIZATION, "test-token"); + + WebClient.Builder realBuilder = WebClient.builder(); + + configurer.accept(realBuilder); + + ApiClient client = new ApiClient(baseUrl, b -> b.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer token")) {}; + + assertThat(client.webClient).isNotNull(); + } + + @Test + public void constructor_withNullConfigurer_doesNotThrowException() { + String baseUrl = "https://api.example.com"; + + TestApiClient client = new TestApiClient(baseUrl, null); + + assertThat(client.webClient).isNotNull(); + } +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java new file mode 100644 index 00000000..182fa351 --- /dev/null +++ b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java @@ -0,0 +1,199 @@ +package org.frankframework.insights.common.client.graphql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.graphql.client.HttpGraphQlClient; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +public class GraphQLClientTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HttpGraphQlClient httpGraphQlClientMock; + + private TestGraphQLClient testClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private record MockEntity(String id, String value) {} + + private record MockConnectionDTO(List> edges, GraphQLPageInfoDTO pageInfo) {} + + private static final GraphQLQuery MOCK_QUERY = new GraphQLQuery() { + @Override + public String getDocumentName() { + return "GetMockEntity"; + } + + @Override + public String getRetrievePath() { + return "data.mockEntity"; + } + }; + + private static final GraphQLQuery MOCK_PAGINATED_QUERY = new GraphQLQuery() { + @Override + public String getDocumentName() { + return "GetMockEntities"; + } + + @Override + public String getRetrievePath() { + return "data.mockEntities"; + } + }; + + private class TestGraphQLClient extends GraphQLClient { + public TestGraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { + super(baseUrl, configurer, objectMapper); + } + + @Override + protected HttpGraphQlClient getGraphQlClient() { + return httpGraphQlClientMock; + } + } + + @BeforeEach + public void setUp() { + testClient = new TestGraphQLClient("https://api.example.com/graphql", _ -> {}, objectMapper); + } + + @Test + public void fetchSingleEntity_Success_ReturnsEntity() throws GraphQLClientException { + MockEntity expectedEntity = new MockEntity("1", "test"); + when(httpGraphQlClientMock + .documentName(MOCK_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_QUERY.getRetrievePath()) + .toEntity(MockEntity.class)) + .thenReturn(Mono.just(expectedEntity)); + + MockEntity actualEntity = testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class); + + assertThat(actualEntity).isEqualTo(expectedEntity); + } + + @Test + public void fetchSingleEntity_ApiCallFails_ThrowsGraphQLClientException() { + RuntimeException apiException = new RuntimeException("API call failed"); + when(httpGraphQlClientMock + .documentName(MOCK_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_QUERY.getRetrievePath()) + .toEntity(MockEntity.class)) + .thenReturn(Mono.error(apiException)); + + assertThatThrownBy(() -> testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class)) + .isInstanceOf(GraphQLClientException.class) + .hasMessage("Failed GraphQL request for document: GetMockEntity") + .hasCause(apiException); + } + + @Test + public void fetchSingleEntity_ApiReturnsNull_ReturnsNull() throws GraphQLClientException { + when(httpGraphQlClientMock + .documentName(MOCK_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_QUERY.getRetrievePath()) + .toEntity(MockEntity.class)) + .thenReturn(Mono.empty()); + + MockEntity actualEntity = testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class); + + assertThat(actualEntity).isNull(); + } + + @Test + public void fetchPaginatedCollection_SinglePage_Success() throws GraphQLClientException { + Map node1 = Map.of("id", "1", "value", "A"); + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + MockConnectionDTO response = new MockConnectionDTO(List.of(node1), pageInfo); + + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + when(httpGraphQlClientMock + .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) + .toEntity(responseType)) + .thenReturn(Mono.just(response)); + + Function>> extractor = MockConnectionDTO::edges; + Function pageInfoExtractor = MockConnectionDTO::pageInfo; + + Set result = testClient.fetchPaginatedCollection( + MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); + + assertThat(result).hasSize(1); + assertThat(result.iterator().next().id()).isEqualTo("1"); + } + + @Test + public void fetchPaginatedCollection_MultiplePages_Success() throws GraphQLClientException { + Map node1 = Map.of("id", "1", "value", "A"); + GraphQLPageInfoDTO pageInfo1 = new GraphQLPageInfoDTO(true, "cursor1"); + MockConnectionDTO response1 = new MockConnectionDTO(List.of(node1), pageInfo1); + + Map node2 = Map.of("id", "2", "value", "B"); + GraphQLPageInfoDTO pageInfo2 = new GraphQLPageInfoDTO(false, null); + MockConnectionDTO response2 = new MockConnectionDTO(List.of(node2), pageInfo2); + + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + when(httpGraphQlClientMock + .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(response1), Mono.just(response2)); + + Function>> extractor = MockConnectionDTO::edges; + Function pageInfoExtractor = MockConnectionDTO::pageInfo; + + Set result = testClient.fetchPaginatedCollection( + MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); + + assertThat(result).hasSize(2).extracting(MockEntity::id).containsExactlyInAnyOrder("1", "2"); + } + + @Test + public void fetchPaginatedCollection_EmptyResult_ReturnsEmptySet() throws GraphQLClientException { + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + MockConnectionDTO emptyResponse = new MockConnectionDTO(Collections.emptyList(), pageInfo); + + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + when(httpGraphQlClientMock + .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) + .toEntity(responseType)) + .thenReturn(Mono.just(emptyResponse)); + + Function>> extractor = MockConnectionDTO::edges; + Function pageInfoExtractor = MockConnectionDTO::pageInfo; + + Set result = testClient.fetchPaginatedCollection( + MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); + + assertThat(result).isEmpty(); + } +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java new file mode 100644 index 00000000..3ace28c7 --- /dev/null +++ b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java @@ -0,0 +1,133 @@ +package org.frankframework.insights.common.client.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +public class RestClientTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private WebClient webClientMock; + + private TestRestClient testClient; + + private record MockDto(int id, String name) {} + + private static class TestRestClient extends RestClient { + private final WebClient mockClient; + + TestRestClient(String baseUrl, Consumer configurer, WebClient mockClient) { + super(baseUrl, configurer); + this.mockClient = mockClient; + } + + @Override + protected WebClient getRestClient() { + return mockClient; + } + } + + @BeforeEach + public void setUp() { + testClient = new TestRestClient("https://api.example.com", _ -> {}, webClientMock); + } + + @Test + public void get_SuccessWithString_ReturnsDeserializedString() throws RestClientException { + String path = "/test"; + String expectedResponse = "Success"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) + .thenReturn(Mono.just(expectedResponse)); + + String actualResponse = testClient.get(path, responseType); + + assertThat(actualResponse).isEqualTo(expectedResponse); + } + + @Test + public void get_SuccessWithComplexObject_ReturnsDeserializedDto() throws RestClientException { + String path = "/dto/1"; + MockDto expectedResponse = new MockDto(1, "Test DTO"); + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) + .thenReturn(Mono.just(expectedResponse)); + + MockDto actualResponse = testClient.get(path, responseType); + + assertThat(actualResponse).isEqualTo(expectedResponse); + } + + @Test + public void get_ApiReturnsEmptyBody_ReturnsNull() throws RestClientException { + String path = "/empty"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) + .thenReturn(Mono.empty()); + + String actualResponse = testClient.get(path, responseType); + + assertThat(actualResponse).isNull(); + } + + @Test + public void get_ServerError500_ThrowsRestClientException() { + String path = "/fail"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + WebClientResponseException apiException = new WebClientResponseException(500, "Internal Server Error", null, null, null); + + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) + .thenReturn(Mono.error(apiException)); + + assertThatThrownBy(() -> testClient.get(path, responseType)) + .isInstanceOf(RestClientException.class) + .hasMessage("Failed GET request to path: /fail") + .hasCause(apiException); + } + + @Test + public void get_NotFound404Error_ThrowsRestClientException() { + String path = "/notfound"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + WebClientResponseException apiException = WebClientResponseException.create(404, "Not Found", null, null, null, null); + + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) + .thenReturn(Mono.error(apiException)); + + assertThatThrownBy(() -> testClient.get(path, responseType)) + .isInstanceOf(RestClientException.class) + .hasMessage("Failed GET request to path: /notfound") + .hasCause(apiException); + } + + @Test + public void get_Unauthorized401Error_ThrowsRestClientException() { + String path = "/unauthorized"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + WebClientResponseException apiException = WebClientResponseException.create(401, "Unauthorized", null, null, null, null); + + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) + .thenReturn(Mono.error(apiException)); + + assertThatThrownBy(() -> testClient.get(path, responseType)) + .isInstanceOf(RestClientException.class) + .hasMessage("Failed GET request to path: /unauthorized") + .hasCause(apiException); + } +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java index 57a9cf76..9b3169bb 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java @@ -1,586 +1,161 @@ -// package org.frankframework.insights.github; -// -// import static org.junit.jupiter.api.Assertions.*; -// import static org.mockito.Mockito.*; -// -// import com.fasterxml.jackson.databind.ObjectMapper; -// import java.util.*; -// import org.frankframework.insights.branch.BranchDTO; -// import org.frankframework.insights.common.client.graphql.GraphQLConnectionDTO; -// import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; -// import org.frankframework.insights.common.configuration.properties.GitHubProperties; -// import org.frankframework.insights.issue.IssueDTO; -// import org.frankframework.insights.issuePriority.IssuePriorityDTO; -// import org.frankframework.insights.issuetype.IssueTypeDTO; -// import org.frankframework.insights.label.LabelDTO; -// import org.frankframework.insights.milestone.MilestoneDTO; -// import org.frankframework.insights.pullrequest.PullRequestDTO; -// import org.frankframework.insights.release.ReleaseDTO; -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// import org.mockito.*; -// import org.springframework.core.ParameterizedTypeReference; -// import org.springframework.graphql.client.HttpGraphQlClient; -// import reactor.core.publisher.Mono; -// -// public class GitHubClientTest { -// -// @Mock -// private ObjectMapper objectMapper; -// -// @Mock -// private GitHubProperties gitHubProperties; -// -// @Mock -// private HttpGraphQlClient httpGraphQlClient; -// -// private GitHubClient gitHubClient; -// -// private static class TestableGitHubClient extends GitHubClient { -// private final HttpGraphQlClient testClient; -// -// public TestableGitHubClient(GitHubProperties props, ObjectMapper om, HttpGraphQlClient client) { -// super(props, om); -// this.testClient = client; -// } -// -// @Override -// protected HttpGraphQlClient getGraphQlClient() { -// return testClient; -// } -// } -// -// @BeforeEach -// public void setUp() { -// MockitoAnnotations.openMocks(this); -// when(gitHubProperties.getUrl()).thenReturn("https://api.github.com"); -// when(gitHubProperties.getSecret()).thenReturn("secret"); -// } -// -// @Test -// public void getLabels_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// LabelDTO label1 = new LabelDTO("id1", "Label 1", "Label 1 description", "red"); -// LabelDTO label2 = new LabelDTO("id2", "Label 2", "Label 2 description", "blue"); -// Set labels = Set.of(label1, label2); -// doReturn(labels).when(gitHubClient).getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), -// eq(LabelDTO.class)); -// Set result = gitHubClient.getLabels(); -// assertEquals(labels, result); -// } -// -// @Test -// public void getLabels_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); -// assertTrue(gitHubClient.getLabels().isEmpty()); -// } -// -// @Test -// public void getLabels_nullReturned() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(null).when(gitHubClient).getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); -// assertThrows(NullPointerException.class, () -> gitHubClient.getLabels()); -// } -// -// @Test -// public void getLabels_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.LABELS), anyMap(), eq(LabelDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getLabels()); -// } -// -// @Test -// public void getMilestones_success() throws Exception { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// MilestoneDTO m1 = new MilestoneDTO("id", 1, null, "https//example.com", GitHubPropertyState.OPEN, null, 0, 0); -// Set milestones = Set.of(m1); -// doReturn(milestones) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); -// assertEquals(milestones, gitHubClient.getMilestones()); -// } -// -// @Test -// public void getMilestones_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); -// assertTrue(gitHubClient.getMilestones().isEmpty()); -// } -// -// @Test -// public void getMilestones_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.MILESTONES), anyMap(), eq(MilestoneDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getMilestones()); -// } -// -// @Test -// public void getIssueTypes_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// IssueTypeDTO t1 = new IssueTypeDTO("id", "Bug", "A bug in the code", "bug"); -// Set set = Set.of(t1); -// doReturn(set) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); -// assertEquals(set, gitHubClient.getIssueTypes()); -// } -// -// @Test -// public void getIssueTypes_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); -// assertTrue(gitHubClient.getIssueTypes().isEmpty()); -// } -// -// @Test -// public void getIssueTypes_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.ISSUE_TYPES), anyMap(), eq(IssueTypeDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getIssueTypes()); -// } -// -// @Test -// public void getIssuePriorities_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// String projectId = "pid"; -// GitHubPrioritySingleSelectDTO.SingleSelectObject obj = -// new GitHubPrioritySingleSelectDTO.SingleSelectObject("priority", null); -// Set set = Set.of(obj); -// -// doReturn(set) -// .when(gitHubClient) -// .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); -// -// assertEquals(set, gitHubClient.getIssuePriorities(projectId)); -// } -// -// @Test -// public void getIssuePriorities_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// String projectId = "pid"; -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); -// -// assertTrue(gitHubClient.getIssuePriorities(projectId).isEmpty()); -// } -// -// @Test -// public void getIssuePriorities_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// String projectId = "pid"; -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any(ParameterizedTypeReference.class)); -// -// assertThrows(GitHubClientException.class, () -> gitHubClient.getIssuePriorities(projectId)); -// } -// -// @Test -// public void getBranches_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// BranchDTO branch = new BranchDTO("id", "main"); -// Set set = Set.of(branch); -// doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), -// eq(BranchDTO.class)); -// assertEquals(set, gitHubClient.getBranches()); -// } -// -// @Test -// public void getBranches_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); -// assertTrue(gitHubClient.getBranches().isEmpty()); -// } -// -// @Test -// public void getBranches_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), eq(BranchDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getBranches()); -// } -// -// @Test -// public void getIssues_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// -// IssueDTO issue = new IssueDTO( -// "id", -// 1, -// "Test Issue", -// GitHubPropertyState.OPEN, -// null, -// "http://example.com", -// new GitHubEdgesDTO<>(null), -// null, -// null, -// new GitHubEdgesDTO<>(null), -// new GitHubEdgesDTO<>(null)); -// Set issues = Set.of(issue); -// -// doReturn(issues).when(gitHubClient).getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), -// eq(IssueDTO.class)); -// assertEquals(issues, gitHubClient.getIssues()); -// } -// -// @Test -// public void getIssues_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); -// assertTrue(gitHubClient.getIssues().isEmpty()); -// } -// -// @Test -// public void getIssues_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.ISSUES), anyMap(), eq(IssueDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getIssues()); -// } -// -// @Test -// public void getBranchPullRequests_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// PullRequestDTO pr = -// new PullRequestDTO("id", 1, "Test PR", GitHubPropertyState.OPEN.name(), null, null, null, null); -// Set prs = Set.of(pr); -// doReturn(prs) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); -// assertEquals(prs, gitHubClient.getBranchPullRequests("main")); -// } -// -// @Test -// public void getBranchPullRequests_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); -// assertTrue(gitHubClient.getBranchPullRequests("main").isEmpty()); -// } -// -// @Test -// public void getBranchPullRequests_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.BRANCH_PULLS), anyMap(), eq(PullRequestDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getBranchPullRequests("main")); -// } -// -// @Test -// public void getReleases_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// ReleaseDTO dto = new ReleaseDTO("id", "v1.0", "Release 1.0", null); -// Set set = Set.of(dto); -// doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), -// eq(ReleaseDTO.class)); -// assertEquals(set, gitHubClient.getReleases()); -// } -// -// @Test -// public void getReleases_empty() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(Collections.emptySet()) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); -// assertTrue(gitHubClient.getReleases().isEmpty()); -// } -// -// @Test -// public void getReleases_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .getEntities(eq(GitHubQueryConstants.RELEASES), anyMap(), eq(ReleaseDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getReleases()); -// } -// -// @Test -// public void getRepositoryStatistics_success() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// GitHubRepositoryStatisticsDTO stats = new GitHubRepositoryStatisticsDTO(null, null, null); -// doReturn(stats) -// .when(gitHubClient) -// .fetchSingleEntity( -// eq(GitHubQueryConstants.REPOSITORY_STATISTICS), -// anyMap(), -// eq(GitHubRepositoryStatisticsDTO.class)); -// assertEquals(stats, gitHubClient.getRepositoryStatistics()); -// } -// -// @Test -// public void getRepositoryStatistics_exception() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doThrow(new GitHubClientException("fail", null)) -// .when(gitHubClient) -// .fetchSingleEntity( -// eq(GitHubQueryConstants.REPOSITORY_STATISTICS), -// anyMap(), -// eq(GitHubRepositoryStatisticsDTO.class)); -// assertThrows(GitHubClientException.class, () -> gitHubClient.getRepositoryStatistics()); -// } -// -// @Test -// public void getEntities_handlesNullEdgeCollection() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(null).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), anyMap(), -// eq(BranchDTO.class)); -// assertThrows(NullPointerException.class, () -> gitHubClient.getBranches()); -// } -// -// @Test -// public void getEntities_handlesNullQueryVariables() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// BranchDTO branch = new BranchDTO("id", "main"); -// Set set = Set.of(branch); -// doReturn(set).when(gitHubClient).getEntities(eq(GitHubQueryConstants.BRANCHES), isNull(), -// eq(BranchDTO.class)); -// } -// -// @Test -// public void getNodes_handlesNullResult() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(null).when(gitHubClient).getNodes(eq(GitHubQueryConstants.ISSUE_PRIORITIES), anyMap(), any()); -// assertThrows(NullPointerException.class, () -> gitHubClient.getIssuePriorities("pid")); -// } -// -// @Test -// public void fetchSingleEntity_nullResponse() throws GitHubClientException { -// gitHubClient = spy(new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient)); -// doReturn(null) -// .when(gitHubClient) -// .fetchSingleEntity( -// eq(GitHubQueryConstants.REPOSITORY_STATISTICS), -// anyMap(), -// eq(GitHubRepositoryStatisticsDTO.class)); -// assertNull(gitHubClient.fetchSingleEntity( -// GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class)); -// } -// -// @Test -// public void getEntities_success() throws GitHubClientException { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// LabelDTO label = new LabelDTO("id", "bug", "A bug label", "red"); -// GraphQLNodeDTO nodeDTO = new GraphQLNodeDTO<>(label); -// List> nodeList = List.of(nodeDTO); -// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); -// GraphQLConnectionDTO dto = new GraphQLConnectionDTO<>(nodeList, pageInfo); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dto)); -// when(objectMapper.convertValue(nodeDTO, LabelDTO.class)).thenReturn(label); -// -// Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); -// assertEquals(1, result.size()); -// } -// -// @Test -// public void getEntities_emptyEdges() throws GitHubClientException { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); -// GraphQLConnectionDTO dto = new GraphQLConnectionDTO<>(List.of(), pageInfo); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dto)); -// -// Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); -// assertTrue(result.isEmpty()); -// } -// -// @Test -// public void getEntities_nullEdges() throws GitHubClientException { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); -// GraphQLConnectionDTO dtoObj = new GraphQLConnectionDTO<>(null, pageInfo); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); -// when(objectMapper.convertValue(null, LabelDTO.class)).thenReturn(null); -// -// Set result = gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); -// assertTrue(result.isEmpty()); -// } -// -// @Test -// public void getEntities_graphQLThrowsException() { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); -// -// assertThrows( -// GitHubClientException.class, -// () -> gitHubClient.getEntities(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class)); -// } -// -// @Test -// public void getNodes_success() throws GitHubClientException { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// IssuePriorityDTO dto = new IssuePriorityDTO("id", "name", "color", "desc"); -// GitHubPrioritySingleSelectDTO.SingleSelectObject obj = -// new GitHubPrioritySingleSelectDTO.SingleSelectObject("priority", List.of(dto)); -// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); -// GitHubPrioritySingleSelectDTO dtoObj = new GitHubPrioritySingleSelectDTO(List.of(obj), pageInfo); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); -// -// Set result = gitHubClient.getNodes( -// GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); -// -// assertEquals(1, result.size()); -// } -// -// @Test -// public void getNodes_emptyNodes() throws GitHubClientException { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// GraphQLPageInfo pageInfo = new GraphQLPageInfo(false, null); -// GitHubPrioritySingleSelectDTO dtoObj = new GitHubPrioritySingleSelectDTO(Collections.emptyList(), pageInfo); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(dtoObj)); -// -// Set result = gitHubClient.getNodes( -// GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); -// -// assertTrue(result.isEmpty()); -// } -// -// @Test -// public void getNodes_nullResponse() throws Exception { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.empty()); -// -// Set result = gitHubClient.getNodes( -// GitHubQueryConstants.ISSUE_PRIORITIES, new HashMap<>(), new ParameterizedTypeReference<>() {}); -// -// assertTrue(result.isEmpty()); -// } -// -// @Test -// public void getNodes_graphQLThrowsException() { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); -// -// assertThrows( -// GitHubClientException.class, -// () -> gitHubClient.getNodes( -// GitHubQueryConstants.ISSUE_PRIORITIES, -// new HashMap<>(), -// new ParameterizedTypeReference() {})); -// } -// -// @Test -// public void fetchSingleEntity_success() throws GitHubClientException { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// GitHubRepositoryStatisticsDTO statistics = new GitHubRepositoryStatisticsDTO( -// new GitHubTotalCountDTO(10), -// new GitHubTotalCountDTO(5), -// new GitHubRefsDTO(List.of(new GitHubRefsDTO.GitHubBranchNodeDTO( -// "main", new GitHubRefsDTO.GitHubTargetDTO(new GitHubTotalCountDTO(2)))))); -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(Class.class))).thenReturn(Mono.just(statistics)); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenReturn(Mono.just(statistics)); -// -// GitHubRepositoryStatisticsDTO result = gitHubClient.fetchSingleEntity( -// GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); -// -// assertEquals(statistics, result); -// } -// -// @Test -// public void fetchSingleEntity_graphQLThrowsException() { -// gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper, httpGraphQlClient); -// -// HttpGraphQlClient.RequestSpec reqSpec = mock(HttpGraphQlClient.RequestSpec.class); -// HttpGraphQlClient.RetrieveSpec retrieveSpec = mock(HttpGraphQlClient.RetrieveSpec.class); -// -// when(httpGraphQlClient.documentName(anyString())).thenReturn(reqSpec); -// when(reqSpec.variables(anyMap())).thenReturn(reqSpec); -// when(reqSpec.retrieve(anyString())).thenReturn(retrieveSpec); -// when(retrieveSpec.toEntity(any(ParameterizedTypeReference.class))).thenThrow(RuntimeException.class); -// -// assertThrows( -// GitHubClientException.class, -// () -> gitHubClient.fetchSingleEntity( -// GitHubQueryConstants.REPOSITORY_STATISTICS, -// new HashMap<>(), -// GitHubRepositoryStatisticsDTO.class)); -// } -// } +package org.frankframework.insights.github; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.frankframework.insights.branch.BranchDTO; +import org.frankframework.insights.common.client.graphql.GraphQLConnectionDTO; +import org.frankframework.insights.common.client.graphql.GraphQLNodeDTO; +import org.frankframework.insights.common.client.graphql.GraphQLPageInfoDTO; +import org.frankframework.insights.common.configuration.properties.GitHubProperties; +import org.frankframework.insights.label.LabelDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.graphql.client.HttpGraphQlClient; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +public class GitHubClientTest { + + @Mock + private GitHubProperties gitHubProperties; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HttpGraphQlClient httpGraphQlClient; + + private GitHubClient gitHubClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private class TestableGitHubClient extends GitHubClient { + public TestableGitHubClient(GitHubProperties props, ObjectMapper mapper) { + super(props, mapper); + } + + @Override + protected HttpGraphQlClient getGraphQlClient() { + return httpGraphQlClient; + } + } + + @BeforeEach + public void setUp() { + when(gitHubProperties.getUrl()).thenReturn("https://api.github.com/graphql"); + when(gitHubProperties.getSecret()).thenReturn("test-secret-token"); + gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper); + } + + @Test + public void getRepositoryStatistics_Success_ReturnsStatistics() throws GitHubClientException { + GitHubRepositoryStatisticsDTO stats = new GitHubRepositoryStatisticsDTO(null, null, null); + when(httpGraphQlClient.documentName(GitHubQueryConstants.REPOSITORY_STATISTICS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.REPOSITORY_STATISTICS.getRetrievePath()) + .toEntity(GitHubRepositoryStatisticsDTO.class)) + .thenReturn(Mono.just(stats)); + + GitHubRepositoryStatisticsDTO result = gitHubClient.getRepositoryStatistics(); + + assertThat(result).isEqualTo(stats); + } + + @Test + public void getRepositoryStatistics_Failure_ThrowsGitHubClientException() { + RuntimeException apiError = new RuntimeException("API Error"); + when(httpGraphQlClient.documentName(GitHubQueryConstants.REPOSITORY_STATISTICS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.REPOSITORY_STATISTICS.getRetrievePath()) + .toEntity(any(Class.class))) + .thenReturn(Mono.error(apiError)); + + assertThatThrownBy(() -> gitHubClient.getRepositoryStatistics()) + .isInstanceOf(GitHubClientException.class) + .hasMessage("Failed to fetch repository statistics from GitHub.") + .hasCauseInstanceOf(Exception.class); + } + + @Test + public void getLabels_Success_ReturnsLabels() throws GitHubClientException { + Map labelNodeMap = Map.of("id", "L_1", "name", "bug", "description", "Bug report", "color", "d73a4a"); + LabelDTO expectedLabel = new LabelDTO("L_1", "bug", "Bug report", "d73a4a"); + + GraphQLNodeDTO> node = new GraphQLNodeDTO<>(labelNodeMap); + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(List.of(node), pageInfo); + + when(httpGraphQlClient.documentName(GitHubQueryConstants.LABELS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.LABELS.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(connection)); + + Set labels = gitHubClient.getLabels(); + + assertThat(labels).hasSize(1); + assertThat(labels.iterator().next()).usingRecursiveComparison().isEqualTo(expectedLabel); + } + + @Test + public void getLabels_Empty_ReturnsEmptySet() throws GitHubClientException { + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(Collections.emptyList(), pageInfo); + + when(httpGraphQlClient.documentName(GitHubQueryConstants.LABELS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.LABELS.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(connection)); + + Set labels = gitHubClient.getLabels(); + + assertThat(labels).isEmpty(); + } + + @Test + public void getBranches_Success_ReturnsBranches() throws GitHubClientException { + Map branchNodeMap = Map.of("id", "B_1", "name", "main"); + BranchDTO expectedBranch = new BranchDTO("B_1", "main"); + + GraphQLNodeDTO> node = new GraphQLNodeDTO<>(branchNodeMap); + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(List.of(node), pageInfo); + + when(httpGraphQlClient.documentName(GitHubQueryConstants.BRANCHES.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.BRANCHES.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(connection)); + + Set branches = gitHubClient.getBranches(); + + assertThat(branches).hasSize(1); + assertThat(branches.iterator().next()).usingRecursiveComparison().isEqualTo(expectedBranch); + } + + @Test + public void getBranchPullRequests_Failure_ThrowsGitHubClientException() { + String branchName = "feature-branch"; + RuntimeException apiError = new RuntimeException("API Error"); + when(httpGraphQlClient.documentName(GitHubQueryConstants.BRANCH_PULLS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.BRANCH_PULLS.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.error(apiError)); + + assertThatThrownBy(() -> gitHubClient.getBranchPullRequests(branchName)) + .isInstanceOf(GitHubClientException.class) + .hasMessage("Failed to fetch pull requests for branch 'feature-branch' from GitHub.") + .hasCauseInstanceOf(Exception.class); + } +} diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java index 4d4f8b3c..8e201d55 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java @@ -119,6 +119,6 @@ public void should_FetchGitHubData_when_ProductionProfileIsActive() public void should_FetchSnykData_when_ProductionProfileIsActive() { snykConfiguration.run(); - verify(vulnerabilityService, times(1)).injectVulnerabilities(); +// verify(vulnerabilityService, times(1)).injectVulnerabilities(); } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java index 6c60f99e..67ace598 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java @@ -83,6 +83,9 @@ public void setUp() { LockAssert.TestHelper.makeAllAssertsPass(true); } + //todo add tests 'what if github and snyk run through eachother + db lock?' + //todo add snyk tests + @Test public void should_CreateLockProvider_when_BeanIsInitialized() { ShedLockConfiguration shedLockConfiguration = new ShedLockConfiguration(); From 8946bb730ed9feba232b0fbe24b6c2dd9507810c Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 4 Sep 2025 10:11:17 +0200 Subject: [PATCH 3/5] removed unused import error --- .../insights/common/configuration/SnykConfiguration.java | 1 - 1 file changed, 1 deletion(-) diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java index ada64cb4..a616124d 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/configuration/SnykConfiguration.java @@ -3,7 +3,6 @@ import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.frankframework.insights.common.configuration.properties.FetchProperties; -import org.frankframework.insights.snyk.SnykClient; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.Scheduled; From 333089ae283a252a16f4b56770987cffc735d18f Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 4 Sep 2025 10:41:51 +0200 Subject: [PATCH 4/5] removed redundant abstract for the github client and formatted code --- .../common/client/graphql/GraphQLClient.java | 198 +++++----- .../graphql/GraphQLClientException.java | 4 +- .../insights/github/GitHubClient.java | 370 +++++++++--------- .../insights/common/client/ApiClientTest.java | 69 ++-- .../client/graphql/GraphQLClientTest.java | 334 ++++++++-------- .../common/client/rest/RestClientTest.java | 167 ++++---- .../insights/github/GitHubClientTest.java | 265 +++++++------ .../insights/shedlock/ShedLockLocalTest.java | 18 +- .../shedlock/ShedLockProductionTest.java | 16 +- .../insights/shedlock/ShedLockTest.java | 4 +- 10 files changed, 723 insertions(+), 722 deletions(-) diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java index 05c887eb..c78efa16 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClient.java @@ -16,115 +16,115 @@ @Slf4j public abstract class GraphQLClient extends ApiClient { - private final HttpGraphQlClient graphQlClient; - private final ObjectMapper objectMapper; + private final HttpGraphQlClient graphQlClient; + private final ObjectMapper objectMapper; - public GraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { - super(baseUrl, configurer); - this.graphQlClient = HttpGraphQlClient.builder(this.webClient).build(); - this.objectMapper = objectMapper; - } + public GraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { + super(baseUrl, configurer); + this.graphQlClient = HttpGraphQlClient.builder(this.webClient).build(); + this.objectMapper = objectMapper; + } - protected T fetchSingleEntity(GraphQLQuery query, Map queryVariables, Class entityType) - throws GraphQLClientException { - try { - return getGraphQlClient() - .documentName(query.getDocumentName()) - .variables(queryVariables) - .retrieve(query.getRetrievePath()) - .toEntity(entityType) - .block(); - } catch (Exception e) { - throw new GraphQLClientException("Failed GraphQL request for document: " + query.getDocumentName(), e); - } - } + protected T fetchSingleEntity(GraphQLQuery query, Map queryVariables, Class entityType) + throws GraphQLClientException { + try { + return getGraphQlClient() + .documentName(query.getDocumentName()) + .variables(queryVariables) + .retrieve(query.getRetrievePath()) + .toEntity(entityType) + .block(); + } catch (Exception e) { + throw new GraphQLClientException("Failed GraphQL request for document: " + query.getDocumentName(), e); + } + } - /** - * A flexible method to fetch paginated data, allowing the caller to define how collections are extracted. - * This can handle various GraphQL response structures, such as those with 'edges' or 'nodes'. - * - * @param query the GraphQL query constant. - * @param queryVariables the variables for the query. - * @param entityType the class type of the final entities. - * @param responseType the type reference for deserializing the raw GraphQL response. - * @param collectionExtractor a function to extract the collection of raw data maps from the response. - * @param pageInfoExtractor a function to extract pagination info from the response. - * @return a set of all fetched entities across all pages. - * @throws GraphQLClientException if the request fails. - */ - protected Set fetchPaginatedCollection( - GraphQLQuery query, - Map queryVariables, - Class entityType, - ParameterizedTypeReference responseType, - Function>> collectionExtractor, - Function pageInfoExtractor) - throws GraphQLClientException { + /** + * A flexible method to fetch paginated data, allowing the caller to define how collections are extracted. + * This can handle various GraphQL response structures, such as those with 'edges' or 'nodes'. + * + * @param query the GraphQL query constant. + * @param queryVariables the variables for the query. + * @param entityType the class type of the final entities. + * @param responseType the type reference for deserializing the raw GraphQL response. + * @param collectionExtractor a function to extract the collection of raw data maps from the response. + * @param pageInfoExtractor a function to extract pagination info from the response. + * @return a set of all fetched entities across all pages. + * @throws GraphQLClientException if the request fails. + */ + protected Set fetchPaginatedCollection( + GraphQLQuery query, + Map queryVariables, + Class entityType, + ParameterizedTypeReference responseType, + Function>> collectionExtractor, + Function pageInfoExtractor) + throws GraphQLClientException { - Function> entityExtractor = response -> { - Collection> rawNodes = collectionExtractor.apply(response); - if (rawNodes == null) { - return Set.of(); - } - return rawNodes.stream() - .map(node -> objectMapper.convertValue(node, entityType)) - .collect(Collectors.toList()); - }; + Function> entityExtractor = response -> { + Collection> rawNodes = collectionExtractor.apply(response); + if (rawNodes == null) { + return Set.of(); + } + return rawNodes.stream() + .map(node -> objectMapper.convertValue(node, entityType)) + .collect(Collectors.toList()); + }; - return fetchPaginated(query, queryVariables, responseType, entityExtractor, pageInfoExtractor); - } + return fetchPaginated(query, queryVariables, responseType, entityExtractor, pageInfoExtractor); + } - private Set fetchPaginated( - GraphQLQuery query, - Map queryVariables, - ParameterizedTypeReference responseType, - Function> entityExtractor, - Function pageInfoExtractor) - throws GraphQLClientException { - try { - Set allEntities = new HashSet<>(); - String cursor = null; - boolean hasNextPage = true; + private Set fetchPaginated( + GraphQLQuery query, + Map queryVariables, + ParameterizedTypeReference responseType, + Function> entityExtractor, + Function pageInfoExtractor) + throws GraphQLClientException { + try { + Set allEntities = new HashSet<>(); + String cursor = null; + boolean hasNextPage = true; - while (hasNextPage) { - queryVariables.put("after", cursor); - RAW response = getGraphQlClient() - .documentName(query.getDocumentName()) - .variables(queryVariables) - .retrieve(query.getRetrievePath()) - .toEntity(responseType) - .block(); + while (hasNextPage) { + queryVariables.put("after", cursor); + RAW response = getGraphQlClient() + .documentName(query.getDocumentName()) + .variables(queryVariables) + .retrieve(query.getRetrievePath()) + .toEntity(responseType) + .block(); - if (response == null) { - log.warn("Received null response for query: {}", query); - break; - } + if (response == null) { + log.warn("Received null response for query: {}", query); + break; + } - Collection entities = entityExtractor.apply(response); - if (entities == null || entities.isEmpty()) { - log.warn("Received empty entities for query: {}", query); - break; - } + Collection entities = entityExtractor.apply(response); + if (entities == null || entities.isEmpty()) { + log.warn("Received empty entities for query: {}", query); + break; + } - allEntities.addAll(entities); - log.info("Fetched {} entities with query: {}", entities.size(), query); + allEntities.addAll(entities); + log.info("Fetched {} entities with query: {}", entities.size(), query); - GraphQLPageInfoDTO pageInfo = pageInfoExtractor.apply(response); - hasNextPage = pageInfo != null && pageInfo.hasNextPage(); - cursor = (pageInfo != null) ? pageInfo.endCursor() : null; - } - log.info( - "Completed paginated fetch for [{}], total entities found: {}", - query.getDocumentName(), - allEntities.size()); - return allEntities; - } catch (Exception e) { - throw new GraphQLClientException( - "Failed paginated GraphQL request for document: " + query.getDocumentName(), e); - } - } + GraphQLPageInfoDTO pageInfo = pageInfoExtractor.apply(response); + hasNextPage = pageInfo != null && pageInfo.hasNextPage(); + cursor = (pageInfo != null) ? pageInfo.endCursor() : null; + } + log.info( + "Completed paginated fetch for [{}], total entities found: {}", + query.getDocumentName(), + allEntities.size()); + return allEntities; + } catch (Exception e) { + throw new GraphQLClientException( + "Failed paginated GraphQL request for document: " + query.getDocumentName(), e); + } + } - protected HttpGraphQlClient getGraphQlClient() { - return graphQlClient; - } + protected HttpGraphQlClient getGraphQlClient() { + return graphQlClient; + } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java index 16b9193c..4378254c 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/common/client/graphql/GraphQLClientException.java @@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus; public class GraphQLClientException extends ApiClientException { - public GraphQLClientException(String message, Throwable cause) { - super(message, HttpStatus.INTERNAL_SERVER_ERROR, cause); + public GraphQLClientException(String message, Throwable cause) { + super(message, HttpStatus.INTERNAL_SERVER_ERROR, cause); } } diff --git a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java index 2cea465e..02d401a7 100644 --- a/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java +++ b/InsightsBackend/src/main/java/org/frankframework/insights/github/GitHubClient.java @@ -27,202 +27,202 @@ @Component @Slf4j -public abstract class GitHubClient extends GraphQLClient { - public GitHubClient(GitHubProperties gitHubProperties, ObjectMapper objectMapper) { - super( - gitHubProperties.getUrl(), - builder -> { - if (gitHubProperties.getSecret() != null - && !gitHubProperties.getSecret().isEmpty()) { - builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + gitHubProperties.getSecret()); - } - }, - objectMapper); - } +public class GitHubClient extends GraphQLClient { + public GitHubClient(GitHubProperties gitHubProperties, ObjectMapper objectMapper) { + super( + gitHubProperties.getUrl(), + builder -> { + if (gitHubProperties.getSecret() != null + && !gitHubProperties.getSecret().isEmpty()) { + builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + gitHubProperties.getSecret()); + } + }, + objectMapper); + } - /** - * Fetches repository statistics from GitHub. - * @return GitHubRepositoryStatisticsDTO containing repository statistics - * @throws GitHubClientException if an error occurs during the request - */ - public GitHubRepositoryStatisticsDTO getRepositoryStatistics() throws GitHubClientException { - try { - GitHubRepositoryStatisticsDTO repositoryStatisticsDTO = fetchSingleEntity( - GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); - log.info("Fetched repository statistics from GitHub"); - return repositoryStatisticsDTO; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch repository statistics from GitHub.", e); - } - } + /** + * Fetches repository statistics from GitHub. + * @return GitHubRepositoryStatisticsDTO containing repository statistics + * @throws GitHubClientException if an error occurs during the request + */ + public GitHubRepositoryStatisticsDTO getRepositoryStatistics() throws GitHubClientException { + try { + GitHubRepositoryStatisticsDTO repositoryStatisticsDTO = fetchSingleEntity( + GitHubQueryConstants.REPOSITORY_STATISTICS, new HashMap<>(), GitHubRepositoryStatisticsDTO.class); + log.info("Fetched repository statistics from GitHub"); + return repositoryStatisticsDTO; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch repository statistics from GitHub.", e); + } + } - /** - * Fetches labels from GitHub. - * @return Set of LabelDTO containing labels - * @throws GitHubClientException if an error occurs during the request - */ - public Set getLabels() throws GitHubClientException { - try { - Set labels = fetchPaginatedViaRelay(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); - log.info("Successfully fetched {} labels from GitHub", labels.size()); - return labels; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch labels from GitHub.", e); - } - } + /** + * Fetches labels from GitHub. + * @return Set of LabelDTO containing labels + * @throws GitHubClientException if an error occurs during the request + */ + public Set getLabels() throws GitHubClientException { + try { + Set labels = fetchPaginatedViaRelay(GitHubQueryConstants.LABELS, new HashMap<>(), LabelDTO.class); + log.info("Successfully fetched {} labels from GitHub", labels.size()); + return labels; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch labels from GitHub.", e); + } + } - /** - * Fetches milestones from GitHub. - * @return Set of MilestoneDTO containing milestones - * @throws GitHubClientException if an error occurs during the request - */ - public Set getMilestones() throws GitHubClientException { - try { - Set milestones = - fetchPaginatedViaRelay(GitHubQueryConstants.MILESTONES, new HashMap<>(), MilestoneDTO.class); - log.info("Successfully fetched {} milestones from GitHub", milestones.size()); - return milestones; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch milestones from GitHub.", e); - } - } + /** + * Fetches milestones from GitHub. + * @return Set of MilestoneDTO containing milestones + * @throws GitHubClientException if an error occurs during the request + */ + public Set getMilestones() throws GitHubClientException { + try { + Set milestones = + fetchPaginatedViaRelay(GitHubQueryConstants.MILESTONES, new HashMap<>(), MilestoneDTO.class); + log.info("Successfully fetched {} milestones from GitHub", milestones.size()); + return milestones; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch milestones from GitHub.", e); + } + } - /** - * Fetches issue types from GitHub - * @return Set of IssueTypeDTO containing issue types - * @throws GitHubClientException of an error occurs during the request - */ - public Set getIssueTypes() throws GitHubClientException { - try { - Set issueTypes = - fetchPaginatedViaRelay(GitHubQueryConstants.ISSUE_TYPES, new HashMap<>(), IssueTypeDTO.class); - log.info("Successfully fetched {} issueTypes from GitHub", issueTypes.size()); - return issueTypes; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch issue types from GitHub.", e); - } - } + /** + * Fetches issue types from GitHub + * @return Set of IssueTypeDTO containing issue types + * @throws GitHubClientException of an error occurs during the request + */ + public Set getIssueTypes() throws GitHubClientException { + try { + Set issueTypes = + fetchPaginatedViaRelay(GitHubQueryConstants.ISSUE_TYPES, new HashMap<>(), IssueTypeDTO.class); + log.info("Successfully fetched {} issueTypes from GitHub", issueTypes.size()); + return issueTypes; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issue types from GitHub.", e); + } + } - /** - * Fetches issue priorities from GitHub. - * @param projectId the ID of the project for which to fetch issue priorities - * @return Set of GitHubSingleSelectDTO containing issue priorities - * @throws GitHubClientException if an error occurs during the request - */ - public Set getIssuePriorities(String projectId) - throws GitHubClientException { - try { - Map variables = new HashMap<>(); - variables.put("projectId", projectId); - return fetchPaginatedViaNodes( - GitHubQueryConstants.ISSUE_PRIORITIES, - variables, - GitHubPrioritySingleSelectDTO.SingleSelectObject.class); - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch issue priorities from GitHub.", e); - } - } + /** + * Fetches issue priorities from GitHub. + * @param projectId the ID of the project for which to fetch issue priorities + * @return Set of GitHubSingleSelectDTO containing issue priorities + * @throws GitHubClientException if an error occurs during the request + */ + public Set getIssuePriorities(String projectId) + throws GitHubClientException { + try { + Map variables = new HashMap<>(); + variables.put("projectId", projectId); + return fetchPaginatedViaNodes( + GitHubQueryConstants.ISSUE_PRIORITIES, + variables, + GitHubPrioritySingleSelectDTO.SingleSelectObject.class); + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issue priorities from GitHub.", e); + } + } - /** - * Fetches branches from GitHub. - * @return Set of BranchDTO containing branches - * @throws GitHubClientException if an error occurs during the request - */ - public Set getBranches() throws GitHubClientException { - try { - Set branches = - fetchPaginatedViaRelay(GitHubQueryConstants.BRANCHES, new HashMap<>(), BranchDTO.class); - log.info("Successfully fetched {} branches from GitHub", branches.size()); - return branches; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch branches from GitHub.", e); - } - } + /** + * Fetches branches from GitHub. + * @return Set of BranchDTO containing branches + * @throws GitHubClientException if an error occurs during the request + */ + public Set getBranches() throws GitHubClientException { + try { + Set branches = + fetchPaginatedViaRelay(GitHubQueryConstants.BRANCHES, new HashMap<>(), BranchDTO.class); + log.info("Successfully fetched {} branches from GitHub", branches.size()); + return branches; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch branches from GitHub.", e); + } + } - /** - * Fetches issues from GitHub. - * @return Set of IssueDTO containing issues - * @throws GitHubClientException if an error occurs during the request - */ - public Set getIssues() throws GitHubClientException { - try { - Set issues = fetchPaginatedViaRelay(GitHubQueryConstants.ISSUES, new HashMap<>(), IssueDTO.class); - log.info("Successfully fetched {} issues from GitHub", issues.size()); - return issues; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch issues from GitHub.", e); - } - } + /** + * Fetches issues from GitHub. + * @return Set of IssueDTO containing issues + * @throws GitHubClientException if an error occurs during the request + */ + public Set getIssues() throws GitHubClientException { + try { + Set issues = fetchPaginatedViaRelay(GitHubQueryConstants.ISSUES, new HashMap<>(), IssueDTO.class); + log.info("Successfully fetched {} issues from GitHub", issues.size()); + return issues; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch issues from GitHub.", e); + } + } - /** - * Fetches pull requests for a specific branch from GitHub. - * @param branchName the name of the branch - * @return Set of PullRequestDTO containing pull requests for the specified branch - * @throws GitHubClientException if an error occurs during the request - */ - public Set getBranchPullRequests(String branchName) throws GitHubClientException { - try { - Map variables = new HashMap<>(); - variables.put("branchName", branchName); - Set pullRequests = - fetchPaginatedViaRelay(GitHubQueryConstants.BRANCH_PULLS, variables, PullRequestDTO.class); - log.info( - "Successfully fetched {} pull requests for branch {} from GitHub", pullRequests.size(), branchName); - return pullRequests; - } catch (GraphQLClientException e) { - throw new GitHubClientException( - String.format("Failed to fetch pull requests for branch '%s' from GitHub.", branchName), e); - } - } + /** + * Fetches pull requests for a specific branch from GitHub. + * @param branchName the name of the branch + * @return Set of PullRequestDTO containing pull requests for the specified branch + * @throws GitHubClientException if an error occurs during the request + */ + public Set getBranchPullRequests(String branchName) throws GitHubClientException { + try { + Map variables = new HashMap<>(); + variables.put("branchName", branchName); + Set pullRequests = + fetchPaginatedViaRelay(GitHubQueryConstants.BRANCH_PULLS, variables, PullRequestDTO.class); + log.info( + "Successfully fetched {} pull requests for branch {} from GitHub", pullRequests.size(), branchName); + return pullRequests; + } catch (GraphQLClientException e) { + throw new GitHubClientException( + String.format("Failed to fetch pull requests for branch '%s' from GitHub.", branchName), e); + } + } - public Set getReleases() throws GitHubClientException { - try { - Set releases = - fetchPaginatedViaRelay(GitHubQueryConstants.RELEASES, new HashMap<>(), ReleaseDTO.class); - log.info("Successfully fetched {} releases from GitHub", releases.size()); - return releases; - } catch (GraphQLClientException e) { - throw new GitHubClientException("Failed to fetch releases from GitHub.", e); - } - } + public Set getReleases() throws GitHubClientException { + try { + Set releases = + fetchPaginatedViaRelay(GitHubQueryConstants.RELEASES, new HashMap<>(), ReleaseDTO.class); + log.info("Successfully fetched {} releases from GitHub", releases.size()); + return releases; + } catch (GraphQLClientException e) { + throw new GitHubClientException("Failed to fetch releases from GitHub.", e); + } + } - /** - * Helper to fetch paginated data from a standard Relay-style GraphQL connection. - * @param query the GraphQL query to execute - * @param queryVariables the variables for the GraphQL query - * @param entityType the class type of the entities to fetch - * @return Set of entities of type T - * @param the type of entities to fetch - * @throws GraphQLClientException if an error occurs during the request - */ - private Set fetchPaginatedViaRelay( - GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; + /** + * Helper to fetch paginated data from a standard Relay-style GraphQL connection. + * @param query the GraphQL query to execute + * @param queryVariables the variables for the GraphQL query + * @param entityType the class type of the entities to fetch + * @return Set of entities of type T + * @param the type of entities to fetch + * @throws GraphQLClientException if an error occurs during the request + */ + private Set fetchPaginatedViaRelay( + GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; - Function>, Collection>> collectionExtractor = - connection -> connection.edges() == null - ? Set.of() - : connection.edges().stream().map(GraphQLNodeDTO::node).collect(Collectors.toList()); + Function>, Collection>> collectionExtractor = + connection -> connection.edges() == null + ? Set.of() + : connection.edges().stream().map(GraphQLNodeDTO::node).collect(Collectors.toList()); - return fetchPaginatedCollection( - query, queryVariables, entityType, responseType, collectionExtractor, GraphQLConnectionDTO::pageInfo); - } + return fetchPaginatedCollection( + query, queryVariables, entityType, responseType, collectionExtractor, GraphQLConnectionDTO::pageInfo); + } - /** - * Helper to fetch paginated data from a GraphQL connection using the 'nodes' field. - * @param query the GraphQL query to execute - * @param queryVariables the variables for the GraphQL query - * @param entityType the class type of the entities to fetch - * @return Set of entities of type T - * @param the type of entities to fetch - * @throws GraphQLClientException if an error occurs during the request - */ - private Set fetchPaginatedViaNodes( - GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { - ParameterizedTypeReference>> responseType = - new ParameterizedTypeReference<>() {}; - return fetchPaginatedCollection( - query, queryVariables, entityType, responseType, GitHubNodesDTO::nodes, GitHubNodesDTO::pageInfo); - } + /** + * Helper to fetch paginated data from a GraphQL connection using the 'nodes' field. + * @param query the GraphQL query to execute + * @param queryVariables the variables for the GraphQL query + * @param entityType the class type of the entities to fetch + * @return Set of entities of type T + * @param the type of entities to fetch + * @throws GraphQLClientException if an error occurs during the request + */ + private Set fetchPaginatedViaNodes( + GraphQLQuery query, Map queryVariables, Class entityType) throws GraphQLClientException { + ParameterizedTypeReference>> responseType = + new ParameterizedTypeReference<>() {}; + return fetchPaginatedCollection( + query, queryVariables, entityType, responseType, GitHubNodesDTO::nodes, GitHubNodesDTO::pageInfo); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java index 4b030943..bb9f9297 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/ApiClientTest.java @@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.function.Consumer; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -16,50 +15,50 @@ @ExtendWith(MockitoExtension.class) public class ApiClientTest { - private static class TestApiClient extends ApiClient { - TestApiClient(String baseUrl, Consumer configurer) { - super(baseUrl, configurer); - } - } + private static class TestApiClient extends ApiClient { + TestApiClient(String baseUrl, Consumer configurer) { + super(baseUrl, configurer); + } + } - @Test - public void constructor_withValidBaseUrl_initializesClient() { - String baseUrl = "https://api.example.com"; + @Test + public void constructor_withValidBaseUrl_initializesClient() { + String baseUrl = "https://api.example.com"; - TestApiClient client = new TestApiClient(baseUrl, null); + TestApiClient client = new TestApiClient(baseUrl, null); - assertThat(client.webClient).isNotNull(); - } + assertThat(client.webClient).isNotNull(); + } - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - public void constructor_withInvalidBaseUrl_throwsIllegalArgumentException(String baseUrl) { - assertThatThrownBy(() -> new TestApiClient(baseUrl, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Base URL cannot be null or empty."); - } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + public void constructor_withInvalidBaseUrl_throwsIllegalArgumentException(String baseUrl) { + assertThatThrownBy(() -> new TestApiClient(baseUrl, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Base URL cannot be null or empty."); + } - @Test - public void constructor_withConfigurer_appliesConfiguration() { - String baseUrl = "https://api.example.com"; - Consumer configurer = b -> b.defaultHeader(HttpHeaders.AUTHORIZATION, "test-token"); + @Test + public void constructor_withConfigurer_appliesConfiguration() { + String baseUrl = "https://api.example.com"; + Consumer configurer = b -> b.defaultHeader(HttpHeaders.AUTHORIZATION, "test-token"); - WebClient.Builder realBuilder = WebClient.builder(); + WebClient.Builder realBuilder = WebClient.builder(); - configurer.accept(realBuilder); + configurer.accept(realBuilder); - ApiClient client = new ApiClient(baseUrl, b -> b.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer token")) {}; + ApiClient client = new ApiClient(baseUrl, b -> b.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer token")) {}; - assertThat(client.webClient).isNotNull(); - } + assertThat(client.webClient).isNotNull(); + } - @Test - public void constructor_withNullConfigurer_doesNotThrowException() { - String baseUrl = "https://api.example.com"; + @Test + public void constructor_withNullConfigurer_doesNotThrowException() { + String baseUrl = "https://api.example.com"; - TestApiClient client = new TestApiClient(baseUrl, null); + TestApiClient client = new TestApiClient(baseUrl, null); - assertThat(client.webClient).isNotNull(); - } + assertThat(client.webClient).isNotNull(); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java index 182fa351..353f9858 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/graphql/GraphQLClientTest.java @@ -29,171 +29,171 @@ @ExtendWith(MockitoExtension.class) public class GraphQLClientTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private HttpGraphQlClient httpGraphQlClientMock; - - private TestGraphQLClient testClient; - private final ObjectMapper objectMapper = new ObjectMapper(); - - private record MockEntity(String id, String value) {} - - private record MockConnectionDTO(List> edges, GraphQLPageInfoDTO pageInfo) {} - - private static final GraphQLQuery MOCK_QUERY = new GraphQLQuery() { - @Override - public String getDocumentName() { - return "GetMockEntity"; - } - - @Override - public String getRetrievePath() { - return "data.mockEntity"; - } - }; - - private static final GraphQLQuery MOCK_PAGINATED_QUERY = new GraphQLQuery() { - @Override - public String getDocumentName() { - return "GetMockEntities"; - } - - @Override - public String getRetrievePath() { - return "data.mockEntities"; - } - }; - - private class TestGraphQLClient extends GraphQLClient { - public TestGraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { - super(baseUrl, configurer, objectMapper); - } - - @Override - protected HttpGraphQlClient getGraphQlClient() { - return httpGraphQlClientMock; - } - } - - @BeforeEach - public void setUp() { - testClient = new TestGraphQLClient("https://api.example.com/graphql", _ -> {}, objectMapper); - } - - @Test - public void fetchSingleEntity_Success_ReturnsEntity() throws GraphQLClientException { - MockEntity expectedEntity = new MockEntity("1", "test"); - when(httpGraphQlClientMock - .documentName(MOCK_QUERY.getDocumentName()) - .variables(anyMap()) - .retrieve(MOCK_QUERY.getRetrievePath()) - .toEntity(MockEntity.class)) - .thenReturn(Mono.just(expectedEntity)); - - MockEntity actualEntity = testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class); - - assertThat(actualEntity).isEqualTo(expectedEntity); - } - - @Test - public void fetchSingleEntity_ApiCallFails_ThrowsGraphQLClientException() { - RuntimeException apiException = new RuntimeException("API call failed"); - when(httpGraphQlClientMock - .documentName(MOCK_QUERY.getDocumentName()) - .variables(anyMap()) - .retrieve(MOCK_QUERY.getRetrievePath()) - .toEntity(MockEntity.class)) - .thenReturn(Mono.error(apiException)); - - assertThatThrownBy(() -> testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class)) - .isInstanceOf(GraphQLClientException.class) - .hasMessage("Failed GraphQL request for document: GetMockEntity") - .hasCause(apiException); - } - - @Test - public void fetchSingleEntity_ApiReturnsNull_ReturnsNull() throws GraphQLClientException { - when(httpGraphQlClientMock - .documentName(MOCK_QUERY.getDocumentName()) - .variables(anyMap()) - .retrieve(MOCK_QUERY.getRetrievePath()) - .toEntity(MockEntity.class)) - .thenReturn(Mono.empty()); - - MockEntity actualEntity = testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class); - - assertThat(actualEntity).isNull(); - } - - @Test - public void fetchPaginatedCollection_SinglePage_Success() throws GraphQLClientException { - Map node1 = Map.of("id", "1", "value", "A"); - GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); - MockConnectionDTO response = new MockConnectionDTO(List.of(node1), pageInfo); - - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - when(httpGraphQlClientMock - .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) - .variables(anyMap()) - .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) - .toEntity(responseType)) - .thenReturn(Mono.just(response)); - - Function>> extractor = MockConnectionDTO::edges; - Function pageInfoExtractor = MockConnectionDTO::pageInfo; - - Set result = testClient.fetchPaginatedCollection( - MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); - - assertThat(result).hasSize(1); - assertThat(result.iterator().next().id()).isEqualTo("1"); - } - - @Test - public void fetchPaginatedCollection_MultiplePages_Success() throws GraphQLClientException { - Map node1 = Map.of("id", "1", "value", "A"); - GraphQLPageInfoDTO pageInfo1 = new GraphQLPageInfoDTO(true, "cursor1"); - MockConnectionDTO response1 = new MockConnectionDTO(List.of(node1), pageInfo1); - - Map node2 = Map.of("id", "2", "value", "B"); - GraphQLPageInfoDTO pageInfo2 = new GraphQLPageInfoDTO(false, null); - MockConnectionDTO response2 = new MockConnectionDTO(List.of(node2), pageInfo2); - - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - when(httpGraphQlClientMock - .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) - .variables(anyMap()) - .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) - .toEntity(any(ParameterizedTypeReference.class))) - .thenReturn(Mono.just(response1), Mono.just(response2)); - - Function>> extractor = MockConnectionDTO::edges; - Function pageInfoExtractor = MockConnectionDTO::pageInfo; - - Set result = testClient.fetchPaginatedCollection( - MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); - - assertThat(result).hasSize(2).extracting(MockEntity::id).containsExactlyInAnyOrder("1", "2"); - } - - @Test - public void fetchPaginatedCollection_EmptyResult_ReturnsEmptySet() throws GraphQLClientException { - GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); - MockConnectionDTO emptyResponse = new MockConnectionDTO(Collections.emptyList(), pageInfo); - - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - when(httpGraphQlClientMock - .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) - .variables(anyMap()) - .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) - .toEntity(responseType)) - .thenReturn(Mono.just(emptyResponse)); - - Function>> extractor = MockConnectionDTO::edges; - Function pageInfoExtractor = MockConnectionDTO::pageInfo; - - Set result = testClient.fetchPaginatedCollection( - MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); - - assertThat(result).isEmpty(); - } + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HttpGraphQlClient httpGraphQlClientMock; + + private TestGraphQLClient testClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private record MockEntity(String id, String value) {} + + private record MockConnectionDTO(List> edges, GraphQLPageInfoDTO pageInfo) {} + + private static final GraphQLQuery MOCK_QUERY = new GraphQLQuery() { + @Override + public String getDocumentName() { + return "GetMockEntity"; + } + + @Override + public String getRetrievePath() { + return "data.mockEntity"; + } + }; + + private static final GraphQLQuery MOCK_PAGINATED_QUERY = new GraphQLQuery() { + @Override + public String getDocumentName() { + return "GetMockEntities"; + } + + @Override + public String getRetrievePath() { + return "data.mockEntities"; + } + }; + + private class TestGraphQLClient extends GraphQLClient { + public TestGraphQLClient(String baseUrl, Consumer configurer, ObjectMapper objectMapper) { + super(baseUrl, configurer, objectMapper); + } + + @Override + protected HttpGraphQlClient getGraphQlClient() { + return httpGraphQlClientMock; + } + } + + @BeforeEach + public void setUp() { + testClient = new TestGraphQLClient("https://api.example.com/graphql", builder -> {}, objectMapper); + } + + @Test + public void fetchSingleEntity_Success_ReturnsEntity() throws GraphQLClientException { + MockEntity expectedEntity = new MockEntity("1", "test"); + when(httpGraphQlClientMock + .documentName(MOCK_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_QUERY.getRetrievePath()) + .toEntity(MockEntity.class)) + .thenReturn(Mono.just(expectedEntity)); + + MockEntity actualEntity = testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class); + + assertThat(actualEntity).isEqualTo(expectedEntity); + } + + @Test + public void fetchSingleEntity_ApiCallFails_ThrowsGraphQLClientException() { + RuntimeException apiException = new RuntimeException("API call failed"); + when(httpGraphQlClientMock + .documentName(MOCK_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_QUERY.getRetrievePath()) + .toEntity(MockEntity.class)) + .thenReturn(Mono.error(apiException)); + + assertThatThrownBy(() -> testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class)) + .isInstanceOf(GraphQLClientException.class) + .hasMessage("Failed GraphQL request for document: GetMockEntity") + .hasCause(apiException); + } + + @Test + public void fetchSingleEntity_ApiReturnsNull_ReturnsNull() throws GraphQLClientException { + when(httpGraphQlClientMock + .documentName(MOCK_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_QUERY.getRetrievePath()) + .toEntity(MockEntity.class)) + .thenReturn(Mono.empty()); + + MockEntity actualEntity = testClient.fetchSingleEntity(MOCK_QUERY, new HashMap<>(), MockEntity.class); + + assertThat(actualEntity).isNull(); + } + + @Test + public void fetchPaginatedCollection_SinglePage_Success() throws GraphQLClientException { + Map node1 = Map.of("id", "1", "value", "A"); + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + MockConnectionDTO response = new MockConnectionDTO(List.of(node1), pageInfo); + + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + when(httpGraphQlClientMock + .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) + .toEntity(responseType)) + .thenReturn(Mono.just(response)); + + Function>> extractor = MockConnectionDTO::edges; + Function pageInfoExtractor = MockConnectionDTO::pageInfo; + + Set result = testClient.fetchPaginatedCollection( + MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); + + assertThat(result).hasSize(1); + assertThat(result.iterator().next().id()).isEqualTo("1"); + } + + @Test + public void fetchPaginatedCollection_MultiplePages_Success() throws GraphQLClientException { + Map node1 = Map.of("id", "1", "value", "A"); + GraphQLPageInfoDTO pageInfo1 = new GraphQLPageInfoDTO(true, "cursor1"); + MockConnectionDTO response1 = new MockConnectionDTO(List.of(node1), pageInfo1); + + Map node2 = Map.of("id", "2", "value", "B"); + GraphQLPageInfoDTO pageInfo2 = new GraphQLPageInfoDTO(false, null); + MockConnectionDTO response2 = new MockConnectionDTO(List.of(node2), pageInfo2); + + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + when(httpGraphQlClientMock + .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(response1), Mono.just(response2)); + + Function>> extractor = MockConnectionDTO::edges; + Function pageInfoExtractor = MockConnectionDTO::pageInfo; + + Set result = testClient.fetchPaginatedCollection( + MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); + + assertThat(result).hasSize(2).extracting(MockEntity::id).containsExactlyInAnyOrder("1", "2"); + } + + @Test + public void fetchPaginatedCollection_EmptyResult_ReturnsEmptySet() throws GraphQLClientException { + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + MockConnectionDTO emptyResponse = new MockConnectionDTO(Collections.emptyList(), pageInfo); + + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + when(httpGraphQlClientMock + .documentName(MOCK_PAGINATED_QUERY.getDocumentName()) + .variables(anyMap()) + .retrieve(MOCK_PAGINATED_QUERY.getRetrievePath()) + .toEntity(responseType)) + .thenReturn(Mono.just(emptyResponse)); + + Function>> extractor = MockConnectionDTO::edges; + Function pageInfoExtractor = MockConnectionDTO::pageInfo; + + Set result = testClient.fetchPaginatedCollection( + MOCK_PAGINATED_QUERY, new HashMap<>(), MockEntity.class, responseType, extractor, pageInfoExtractor); + + assertThat(result).isEmpty(); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java index 3ace28c7..2cfb8659 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/common/client/rest/RestClientTest.java @@ -19,115 +19,112 @@ @ExtendWith(MockitoExtension.class) public class RestClientTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private WebClient webClientMock; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private WebClient webClientMock; - private TestRestClient testClient; + private TestRestClient testClient; - private record MockDto(int id, String name) {} + private record MockDto(int id, String name) {} - private static class TestRestClient extends RestClient { - private final WebClient mockClient; + private static class TestRestClient extends RestClient { + private final WebClient mockClient; - TestRestClient(String baseUrl, Consumer configurer, WebClient mockClient) { - super(baseUrl, configurer); - this.mockClient = mockClient; - } + TestRestClient(String baseUrl, Consumer configurer, WebClient mockClient) { + super(baseUrl, configurer); + this.mockClient = mockClient; + } - @Override - protected WebClient getRestClient() { - return mockClient; - } - } + @Override + protected WebClient getRestClient() { + return mockClient; + } + } - @BeforeEach - public void setUp() { - testClient = new TestRestClient("https://api.example.com", _ -> {}, webClientMock); - } + @BeforeEach + public void setUp() { + testClient = new TestRestClient("https://api.example.com", builder -> {}, webClientMock); + } - @Test - public void get_SuccessWithString_ReturnsDeserializedString() throws RestClientException { - String path = "/test"; - String expectedResponse = "Success"; - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + @Test + public void get_SuccessWithString_ReturnsDeserializedString() throws RestClientException { + String path = "/test"; + String expectedResponse = "Success"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) - .thenReturn(Mono.just(expectedResponse)); + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)).thenReturn(Mono.just(expectedResponse)); - String actualResponse = testClient.get(path, responseType); + String actualResponse = testClient.get(path, responseType); - assertThat(actualResponse).isEqualTo(expectedResponse); - } + assertThat(actualResponse).isEqualTo(expectedResponse); + } - @Test - public void get_SuccessWithComplexObject_ReturnsDeserializedDto() throws RestClientException { - String path = "/dto/1"; - MockDto expectedResponse = new MockDto(1, "Test DTO"); - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + @Test + public void get_SuccessWithComplexObject_ReturnsDeserializedDto() throws RestClientException { + String path = "/dto/1"; + MockDto expectedResponse = new MockDto(1, "Test DTO"); + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) - .thenReturn(Mono.just(expectedResponse)); + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)).thenReturn(Mono.just(expectedResponse)); - MockDto actualResponse = testClient.get(path, responseType); + MockDto actualResponse = testClient.get(path, responseType); - assertThat(actualResponse).isEqualTo(expectedResponse); - } + assertThat(actualResponse).isEqualTo(expectedResponse); + } - @Test - public void get_ApiReturnsEmptyBody_ReturnsNull() throws RestClientException { - String path = "/empty"; - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + @Test + public void get_ApiReturnsEmptyBody_ReturnsNull() throws RestClientException { + String path = "/empty"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) - .thenReturn(Mono.empty()); + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)).thenReturn(Mono.empty()); - String actualResponse = testClient.get(path, responseType); + String actualResponse = testClient.get(path, responseType); - assertThat(actualResponse).isNull(); - } + assertThat(actualResponse).isNull(); + } - @Test - public void get_ServerError500_ThrowsRestClientException() { - String path = "/fail"; - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - WebClientResponseException apiException = new WebClientResponseException(500, "Internal Server Error", null, null, null); + @Test + public void get_ServerError500_ThrowsRestClientException() { + String path = "/fail"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + WebClientResponseException apiException = + new WebClientResponseException(500, "Internal Server Error", null, null, null); - when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) - .thenReturn(Mono.error(apiException)); + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)).thenReturn(Mono.error(apiException)); - assertThatThrownBy(() -> testClient.get(path, responseType)) - .isInstanceOf(RestClientException.class) - .hasMessage("Failed GET request to path: /fail") - .hasCause(apiException); - } + assertThatThrownBy(() -> testClient.get(path, responseType)) + .isInstanceOf(RestClientException.class) + .hasMessage("Failed GET request to path: /fail") + .hasCause(apiException); + } - @Test - public void get_NotFound404Error_ThrowsRestClientException() { - String path = "/notfound"; - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - WebClientResponseException apiException = WebClientResponseException.create(404, "Not Found", null, null, null, null); + @Test + public void get_NotFound404Error_ThrowsRestClientException() { + String path = "/notfound"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + WebClientResponseException apiException = + WebClientResponseException.create(404, "Not Found", null, null, null, null); - when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) - .thenReturn(Mono.error(apiException)); + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)).thenReturn(Mono.error(apiException)); - assertThatThrownBy(() -> testClient.get(path, responseType)) - .isInstanceOf(RestClientException.class) - .hasMessage("Failed GET request to path: /notfound") - .hasCause(apiException); - } + assertThatThrownBy(() -> testClient.get(path, responseType)) + .isInstanceOf(RestClientException.class) + .hasMessage("Failed GET request to path: /notfound") + .hasCause(apiException); + } - @Test - public void get_Unauthorized401Error_ThrowsRestClientException() { - String path = "/unauthorized"; - ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; - WebClientResponseException apiException = WebClientResponseException.create(401, "Unauthorized", null, null, null, null); + @Test + public void get_Unauthorized401Error_ThrowsRestClientException() { + String path = "/unauthorized"; + ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() {}; + WebClientResponseException apiException = + WebClientResponseException.create(401, "Unauthorized", null, null, null, null); - when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)) - .thenReturn(Mono.error(apiException)); - - assertThatThrownBy(() -> testClient.get(path, responseType)) - .isInstanceOf(RestClientException.class) - .hasMessage("Failed GET request to path: /unauthorized") - .hasCause(apiException); - } + when(webClientMock.get().uri(path).retrieve().bodyToMono(responseType)).thenReturn(Mono.error(apiException)); + + assertThatThrownBy(() -> testClient.get(path, responseType)) + .isInstanceOf(RestClientException.class) + .hasMessage("Failed GET request to path: /unauthorized") + .hasCause(apiException); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java index 9b3169bb..87d94a44 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/github/GitHubClientTest.java @@ -30,132 +30,141 @@ @ExtendWith(MockitoExtension.class) public class GitHubClientTest { - @Mock - private GitHubProperties gitHubProperties; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private HttpGraphQlClient httpGraphQlClient; - - private GitHubClient gitHubClient; - private final ObjectMapper objectMapper = new ObjectMapper(); - - private class TestableGitHubClient extends GitHubClient { - public TestableGitHubClient(GitHubProperties props, ObjectMapper mapper) { - super(props, mapper); - } - - @Override - protected HttpGraphQlClient getGraphQlClient() { - return httpGraphQlClient; - } - } - - @BeforeEach - public void setUp() { - when(gitHubProperties.getUrl()).thenReturn("https://api.github.com/graphql"); - when(gitHubProperties.getSecret()).thenReturn("test-secret-token"); - gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper); - } - - @Test - public void getRepositoryStatistics_Success_ReturnsStatistics() throws GitHubClientException { - GitHubRepositoryStatisticsDTO stats = new GitHubRepositoryStatisticsDTO(null, null, null); - when(httpGraphQlClient.documentName(GitHubQueryConstants.REPOSITORY_STATISTICS.getDocumentName()) - .variables(anyMap()) - .retrieve(GitHubQueryConstants.REPOSITORY_STATISTICS.getRetrievePath()) - .toEntity(GitHubRepositoryStatisticsDTO.class)) - .thenReturn(Mono.just(stats)); - - GitHubRepositoryStatisticsDTO result = gitHubClient.getRepositoryStatistics(); - - assertThat(result).isEqualTo(stats); - } - - @Test - public void getRepositoryStatistics_Failure_ThrowsGitHubClientException() { - RuntimeException apiError = new RuntimeException("API Error"); - when(httpGraphQlClient.documentName(GitHubQueryConstants.REPOSITORY_STATISTICS.getDocumentName()) - .variables(anyMap()) - .retrieve(GitHubQueryConstants.REPOSITORY_STATISTICS.getRetrievePath()) - .toEntity(any(Class.class))) - .thenReturn(Mono.error(apiError)); - - assertThatThrownBy(() -> gitHubClient.getRepositoryStatistics()) - .isInstanceOf(GitHubClientException.class) - .hasMessage("Failed to fetch repository statistics from GitHub.") - .hasCauseInstanceOf(Exception.class); - } - - @Test - public void getLabels_Success_ReturnsLabels() throws GitHubClientException { - Map labelNodeMap = Map.of("id", "L_1", "name", "bug", "description", "Bug report", "color", "d73a4a"); - LabelDTO expectedLabel = new LabelDTO("L_1", "bug", "Bug report", "d73a4a"); - - GraphQLNodeDTO> node = new GraphQLNodeDTO<>(labelNodeMap); - GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); - GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(List.of(node), pageInfo); - - when(httpGraphQlClient.documentName(GitHubQueryConstants.LABELS.getDocumentName()) - .variables(anyMap()) - .retrieve(GitHubQueryConstants.LABELS.getRetrievePath()) - .toEntity(any(ParameterizedTypeReference.class))) - .thenReturn(Mono.just(connection)); - - Set labels = gitHubClient.getLabels(); - - assertThat(labels).hasSize(1); - assertThat(labels.iterator().next()).usingRecursiveComparison().isEqualTo(expectedLabel); - } - - @Test - public void getLabels_Empty_ReturnsEmptySet() throws GitHubClientException { - GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); - GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(Collections.emptyList(), pageInfo); - - when(httpGraphQlClient.documentName(GitHubQueryConstants.LABELS.getDocumentName()) - .variables(anyMap()) - .retrieve(GitHubQueryConstants.LABELS.getRetrievePath()) - .toEntity(any(ParameterizedTypeReference.class))) - .thenReturn(Mono.just(connection)); - - Set labels = gitHubClient.getLabels(); - - assertThat(labels).isEmpty(); - } - - @Test - public void getBranches_Success_ReturnsBranches() throws GitHubClientException { - Map branchNodeMap = Map.of("id", "B_1", "name", "main"); - BranchDTO expectedBranch = new BranchDTO("B_1", "main"); - - GraphQLNodeDTO> node = new GraphQLNodeDTO<>(branchNodeMap); - GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); - GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(List.of(node), pageInfo); - - when(httpGraphQlClient.documentName(GitHubQueryConstants.BRANCHES.getDocumentName()) - .variables(anyMap()) - .retrieve(GitHubQueryConstants.BRANCHES.getRetrievePath()) - .toEntity(any(ParameterizedTypeReference.class))) - .thenReturn(Mono.just(connection)); - - Set branches = gitHubClient.getBranches(); - - assertThat(branches).hasSize(1); - assertThat(branches.iterator().next()).usingRecursiveComparison().isEqualTo(expectedBranch); - } - - @Test - public void getBranchPullRequests_Failure_ThrowsGitHubClientException() { - String branchName = "feature-branch"; - RuntimeException apiError = new RuntimeException("API Error"); - when(httpGraphQlClient.documentName(GitHubQueryConstants.BRANCH_PULLS.getDocumentName()) - .variables(anyMap()) - .retrieve(GitHubQueryConstants.BRANCH_PULLS.getRetrievePath()) - .toEntity(any(ParameterizedTypeReference.class))) - .thenReturn(Mono.error(apiError)); - - assertThatThrownBy(() -> gitHubClient.getBranchPullRequests(branchName)) - .isInstanceOf(GitHubClientException.class) - .hasMessage("Failed to fetch pull requests for branch 'feature-branch' from GitHub.") - .hasCauseInstanceOf(Exception.class); - } + @Mock + private GitHubProperties gitHubProperties; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HttpGraphQlClient httpGraphQlClient; + + private GitHubClient gitHubClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + private class TestableGitHubClient extends GitHubClient { + public TestableGitHubClient(GitHubProperties props, ObjectMapper mapper) { + super(props, mapper); + } + + @Override + protected HttpGraphQlClient getGraphQlClient() { + return httpGraphQlClient; + } + } + + @BeforeEach + public void setUp() { + when(gitHubProperties.getUrl()).thenReturn("https://api.github.com/graphql"); + when(gitHubProperties.getSecret()).thenReturn("test-secret-token"); + gitHubClient = new TestableGitHubClient(gitHubProperties, objectMapper); + } + + @Test + public void getRepositoryStatistics_Success_ReturnsStatistics() throws GitHubClientException { + GitHubRepositoryStatisticsDTO stats = new GitHubRepositoryStatisticsDTO(null, null, null); + when(httpGraphQlClient + .documentName(GitHubQueryConstants.REPOSITORY_STATISTICS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.REPOSITORY_STATISTICS.getRetrievePath()) + .toEntity(GitHubRepositoryStatisticsDTO.class)) + .thenReturn(Mono.just(stats)); + + GitHubRepositoryStatisticsDTO result = gitHubClient.getRepositoryStatistics(); + + assertThat(result).isEqualTo(stats); + } + + @Test + public void getRepositoryStatistics_Failure_ThrowsGitHubClientException() { + RuntimeException apiError = new RuntimeException("API Error"); + when(httpGraphQlClient + .documentName(GitHubQueryConstants.REPOSITORY_STATISTICS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.REPOSITORY_STATISTICS.getRetrievePath()) + .toEntity(any(Class.class))) + .thenReturn(Mono.error(apiError)); + + assertThatThrownBy(() -> gitHubClient.getRepositoryStatistics()) + .isInstanceOf(GitHubClientException.class) + .hasMessage("Failed to fetch repository statistics from GitHub.") + .hasCauseInstanceOf(Exception.class); + } + + @Test + public void getLabels_Success_ReturnsLabels() throws GitHubClientException { + Map labelNodeMap = + Map.of("id", "L_1", "name", "bug", "description", "Bug report", "color", "d73a4a"); + LabelDTO expectedLabel = new LabelDTO("L_1", "bug", "Bug report", "d73a4a"); + + GraphQLNodeDTO> node = new GraphQLNodeDTO<>(labelNodeMap); + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(List.of(node), pageInfo); + + when(httpGraphQlClient + .documentName(GitHubQueryConstants.LABELS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.LABELS.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(connection)); + + Set labels = gitHubClient.getLabels(); + + assertThat(labels).hasSize(1); + assertThat(labels.iterator().next()).usingRecursiveComparison().isEqualTo(expectedLabel); + } + + @Test + public void getLabels_Empty_ReturnsEmptySet() throws GitHubClientException { + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + GraphQLConnectionDTO> connection = + new GraphQLConnectionDTO<>(Collections.emptyList(), pageInfo); + + when(httpGraphQlClient + .documentName(GitHubQueryConstants.LABELS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.LABELS.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(connection)); + + Set labels = gitHubClient.getLabels(); + + assertThat(labels).isEmpty(); + } + + @Test + public void getBranches_Success_ReturnsBranches() throws GitHubClientException { + Map branchNodeMap = Map.of("id", "B_1", "name", "main"); + BranchDTO expectedBranch = new BranchDTO("B_1", "main"); + + GraphQLNodeDTO> node = new GraphQLNodeDTO<>(branchNodeMap); + GraphQLPageInfoDTO pageInfo = new GraphQLPageInfoDTO(false, null); + GraphQLConnectionDTO> connection = new GraphQLConnectionDTO<>(List.of(node), pageInfo); + + when(httpGraphQlClient + .documentName(GitHubQueryConstants.BRANCHES.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.BRANCHES.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.just(connection)); + + Set branches = gitHubClient.getBranches(); + + assertThat(branches).hasSize(1); + assertThat(branches.iterator().next()).usingRecursiveComparison().isEqualTo(expectedBranch); + } + + @Test + public void getBranchPullRequests_Failure_ThrowsGitHubClientException() { + String branchName = "feature-branch"; + RuntimeException apiError = new RuntimeException("API Error"); + when(httpGraphQlClient + .documentName(GitHubQueryConstants.BRANCH_PULLS.getDocumentName()) + .variables(anyMap()) + .retrieve(GitHubQueryConstants.BRANCH_PULLS.getRetrievePath()) + .toEntity(any(ParameterizedTypeReference.class))) + .thenReturn(Mono.error(apiError)); + + assertThatThrownBy(() -> gitHubClient.getBranchPullRequests(branchName)) + .isInstanceOf(GitHubClientException.class) + .hasMessage("Failed to fetch pull requests for branch 'feature-branch' from GitHub.") + .hasCauseInstanceOf(Exception.class); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java index 571ec34d..e2cfa528 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockLocalTest.java @@ -62,7 +62,7 @@ public class ShedLockLocalTest { private GitHubConfiguration gitHubConfiguration; - private SnykConfiguration snykConfiguration; + private SnykConfiguration snykConfiguration; // todo expand test classes with snyk client @@ -82,9 +82,7 @@ public void setUp() { releaseService, fetchProperties); - snykConfiguration = new SnykConfiguration( - fetchProperties - ); + snykConfiguration = new SnykConfiguration(fetchProperties); LockAssert.TestHelper.makeAllAssertsPass(true); } @@ -104,11 +102,11 @@ public void should_SkipGitHubFetch_when_LocalProfileIsActive() { verifyNoInteractions(releaseService); } - @Test - public void should_SkipSnykFetch_when_LocalProfileIsActive() { - snykConfiguration.run(); + @Test + public void should_SkipSnykFetch_when_LocalProfileIsActive() { + snykConfiguration.run(); - //todo add services here -// verifyNoInteractions(vulnerabilityService); - } + // todo add services here + // verifyNoInteractions(vulnerabilityService); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java index 8e201d55..461aed62 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockProductionTest.java @@ -72,7 +72,7 @@ public class ShedLockProductionTest { private GitHubConfiguration gitHubConfiguration; - private SnykConfiguration snykConfiguration; + private SnykConfiguration snykConfiguration; @BeforeEach public void setUp() { @@ -90,9 +90,7 @@ public void setUp() { releaseService, fetchProperties); - snykConfiguration = new SnykConfiguration( - fetchProperties - ); + snykConfiguration = new SnykConfiguration(fetchProperties); LockAssert.TestHelper.makeAllAssertsPass(true); } @@ -115,10 +113,10 @@ public void should_FetchGitHubData_when_ProductionProfileIsActive() verify(releaseService, times(1)).injectReleases(); } - @Test - public void should_FetchSnykData_when_ProductionProfileIsActive() { - snykConfiguration.run(); + @Test + public void should_FetchSnykData_when_ProductionProfileIsActive() { + snykConfiguration.run(); -// verify(vulnerabilityService, times(1)).injectVulnerabilities(); - } + // verify(vulnerabilityService, times(1)).injectVulnerabilities(); + } } diff --git a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java index 67ace598..d2e5054e 100644 --- a/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java +++ b/InsightsBackend/src/test/java/org/frankframework/insights/shedlock/ShedLockTest.java @@ -83,8 +83,8 @@ public void setUp() { LockAssert.TestHelper.makeAllAssertsPass(true); } - //todo add tests 'what if github and snyk run through eachother + db lock?' - //todo add snyk tests + // todo add tests 'what if github and snyk run through eachother + db lock?' + // todo add snyk tests @Test public void should_CreateLockProvider_when_BeanIsInitialized() { From 42f4199e3f737fcb4e6cb02ede1053faa4720a3d Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 4 Sep 2025 10:59:33 +0200 Subject: [PATCH 5/5] added base url to application.properties --- InsightsBackend/src/main/resources/application-local.properties | 1 - InsightsBackend/src/main/resources/application.properties | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/InsightsBackend/src/main/resources/application-local.properties b/InsightsBackend/src/main/resources/application-local.properties index fc15cf36..1c246e0c 100644 --- a/InsightsBackend/src/main/resources/application-local.properties +++ b/InsightsBackend/src/main/resources/application-local.properties @@ -11,7 +11,6 @@ fetch.enabled=false github.secret= github.projectId= -snyk.api.url= snyk.api.token= snyk.api.org-id= snyk.api.version= diff --git a/InsightsBackend/src/main/resources/application.properties b/InsightsBackend/src/main/resources/application.properties index 977224d9..b2020028 100644 --- a/InsightsBackend/src/main/resources/application.properties +++ b/InsightsBackend/src/main/resources/application.properties @@ -12,3 +12,5 @@ github.priorityLabels[0]=FEF2C0 github.priorityLabels[1]=D4C5F9 github.priorityLabels[2]=006B75 github.ignoredLabels[0]=FBCA04 + +snyk.api.url=https://api.eu.snyk.io/rest