diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/updatecenter/Plugin.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/updatecenter/Plugin.java index 6c51489e7..c3879c48f 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/model/updatecenter/Plugin.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/model/updatecenter/Plugin.java @@ -31,8 +31,27 @@ public record Plugin(String name, VersionNumber version, String scm, ZonedDateTime releaseTimestamp, List labels, - int popularity, String requiredCore, String defaultBranch) { + int popularity, String requiredCore, String defaultBranch, List issueTrackers) { + + /* This builder function is used in test cases when List is not required as a mandatory parameter. */ + public static Plugin of(String name, VersionNumber version, String scm, + ZonedDateTime releaseTimestamp, List labels, + int popularity, String requiredCore, String defaultBranch) { + return new Plugin(name, version, scm, releaseTimestamp, + labels, popularity, requiredCore, defaultBranch, List.of()); + } + public io.jenkins.pluginhealth.scoring.model.Plugin toPlugin() { return new io.jenkins.pluginhealth.scoring.model.Plugin(this.name(), this.version(), this.scm(), this.releaseTimestamp()); } + + /** + * Gets issue tracker details about a plugin. + * + * @param type The type of platform used to track issues. For ex: GitHub, JIRA + * @param reportUrl An url to report issues about the plugin + * @param viewUrl An url to view all the issues in the plugin + */ + public record IssueTrackers(String type, String viewUrl, String reportUrl) { + } } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractOpenIssuesProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractOpenIssuesProbe.java new file mode 100644 index 000000000..07d235d39 --- /dev/null +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/AbstractOpenIssuesProbe.java @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import java.util.Optional; + +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; + +public abstract class AbstractOpenIssuesProbe extends Probe { + public static final int ORDER = IssueTrackerDetectionProbe.ORDER + 100; + + @Override + protected ProbeResult doApply(Plugin plugin, ProbeContext context) { + final Optional openIssuesCount = getCountOfOpenIssues(context); + return openIssuesCount.isPresent() + ? ProbeResult.success(key(), String.format("%d open issues found in the %s plugin.", openIssuesCount.get(), plugin.getName())) + : ProbeResult.failure(key(), String.format("Could not find open issues in the %s plugin.", plugin.getName())); + } + + abstract Optional getCountOfOpenIssues(ProbeContext context); + + @Override + public String[] getProbeResultRequirement() { + return new String[] { SCMLinkValidationProbe.KEY, IssueTrackerDetectionProbe.KEY }; + } +} diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/GitHubOpenIssuesProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/GitHubOpenIssuesProbe.java new file mode 100644 index 000000000..6073d0159 --- /dev/null +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/GitHubOpenIssuesProbe.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import java.io.IOException; +import java.util.Optional; + +import org.kohsuke.github.GHRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(AbstractOpenIssuesProbe.ORDER) +class GitHubOpenIssuesProbe extends AbstractOpenIssuesProbe { + public static final String KEY = "github-open-issues"; + private static final Logger LOGGER = LoggerFactory.getLogger(GitHubOpenIssuesProbe.class); + + /** + * Get total number of open GitHub issues in a plugin. + * + * @param context {@link ProbeContext} + * @return Optional an Integer value will give total count of open issues. + */ + @Override + Optional getCountOfOpenIssues(ProbeContext context) { + if (context.getIssueTrackerUrlsByNames() == null) { + LOGGER.info("IssueTracker has no GitHub open issues for the plugin."); + return Optional.empty(); + } + /* Stores the GitHub URL to view all existing issues in the plugin. Ex: https://github.com/jenkinsci/cloudevents-plugin/issues */ + String issueTrackerViewUrl = context.getIssueTrackerUrlsByNames().get("github"); + + if (issueTrackerViewUrl == null) { + LOGGER.info("The plugin does not use GitHub issues to track issues."); + return Optional.empty(); + } + + try { + final Optional repositoryName = context.getRepositoryName(issueTrackerViewUrl.substring(0, issueTrackerViewUrl.lastIndexOf("/"))); + if (repositoryName.isPresent()) { + final GHRepository ghRepository = context.getGitHub().getRepository(repositoryName.get()); + return Optional.of(ghRepository.getOpenIssueCount()); + } + } catch (IOException ex) { + LOGGER.error("Cannot read open issues on GitHub for the plugin. {}", ex); + } + return Optional.empty(); + } + + @Override + public String key() { + return KEY; + } + + @Override + public String getDescription() { + return "Returns the total number of open issues in GitHub."; + } +} diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IssueTrackerDetectionProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IssueTrackerDetectionProbe.java new file mode 100644 index 000000000..b16593354 --- /dev/null +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/IssueTrackerDetectionProbe.java @@ -0,0 +1,95 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.IssueTrackers; +import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; + +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(value = IssueTrackerDetectionProbe.ORDER) +class IssueTrackerDetectionProbe extends Probe { + public static final String KEY = "issue-tracker-detection"; + public static final int ORDER = UpdateCenterPluginPublicationProbe.ORDER + 100; + + @Override + protected ProbeResult doApply(Plugin plugin, ProbeContext context) { + Map issueTrackerDetails = getIssueTrackersForAPlugin(plugin.getName(), context.getUpdateCenter()); + + if (issueTrackerDetails.isEmpty()) { + return ProbeResult.failure(key(), String.format("No issue tracker data available for %s plugin in Update Center.", plugin.getName())); + } + context.setIssueTrackerNameAndUrl(issueTrackerDetails); + return ProbeResult.success(key(), String.format("Found %d issue trackers configured for the %s plugin.", issueTrackerDetails.size(), plugin.getName())); + } + + @Override + public String[] getProbeResultRequirement() { + return new String[]{UpdateCenterPluginPublicationProbe.KEY}; + } + + @Override + public String key() { + return KEY; + } + + @Override + public String getDescription() { + return "Detects the issues tracker type from Update Center."; + } + + /** + * Gets issue trackers for a specific plugin from Update Center. + * + * @param pluginName name of the plugin to fetch issue tracker data for. + * @param updateCenter The {@link UpdateCenter}. + * @return A Map of filtered data from issue trackers. + */ + private Map getIssueTrackersForAPlugin(String pluginName, UpdateCenter updateCenter) { + return (updateCenter.plugins().get(pluginName) != null) && (updateCenter.plugins().get(pluginName).issueTrackers() != null) + ? filterIssueTrackersForTypeAndViewUrl(updateCenter.plugins().get(pluginName).issueTrackers()) + : Map.of(); + } + + /** + * Filters IssueTrackers for "type" and "viewUrl". + * + * @param issueTrackers Accepts a list of {@link IssueTrackers}. + * @return A Map of {@code IssueTrackers::type} and {@code IssueTrackers::viewUrl}. + */ + private Map filterIssueTrackersForTypeAndViewUrl(List issueTrackers) { + return issueTrackers.stream() + .collect(Collectors.toMap(IssueTrackers::type, IssueTrackers::viewUrl)); + } + +} diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JiraOpenIssuesProbe.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JiraOpenIssuesProbe.java new file mode 100644 index 000000000..31f2832b6 --- /dev/null +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/JiraOpenIssuesProbe.java @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import static java.time.temporal.ChronoUnit.SECONDS; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(AbstractOpenIssuesProbe.ORDER) +class JiraOpenIssuesProbe extends AbstractOpenIssuesProbe { + public static final String KEY = "jira-open-issues"; + private static final String JIRA_HOST = "https://issues.jenkins.io/rest/api/latest/search?"; + private static final Logger LOGGER = LoggerFactory.getLogger(JiraOpenIssuesProbe.class); + final ObjectMapper objectMapper = new ObjectMapper(); + HttpClient httpClient; + HttpRequest httpRequest; + + /** + * Get total number of open JIRA issues in a plugin. + * + * @param context {@link ProbeContext} + * @return Optional of type Integer. + */ + @Override + Optional getCountOfOpenIssues(ProbeContext context) { + if (context.getIssueTrackerUrlsByNames() == null) { + LOGGER.info("IssueTracker has no JIRA open issues for the plugin."); + return Optional.empty(); + } + + /* Stores the JIRA URL to view all existing issues in the plugin. Ex: https://github.com/jenkinsci/cloudevents-plugin/issues */ + String viewJiraIssuesUrl = context.getIssueTrackerUrlsByNames().getOrDefault("jira", ""); + + if (viewJiraIssuesUrl == null || viewJiraIssuesUrl.isEmpty()) { + LOGGER.info("The plugin does not use JIRA to track issues."); + return Optional.empty(); + } + try { + /* The `url` will contain the JIRA url to view issues. + For ex: https://issues.jenkins.io/rest/api/latest/search?jql=component=15979 + */ + URI uri = new URI(viewJiraIssuesUrl); + + /* Here, the query of the url "?jql=component=1833" is concatenated with " AND status=open". + This gives the final API required to fetch JIRA issues. + For ex: https://issues.jenkins.io/rest/api/latest/search?jql=component=15979%20and%20status=open + */ + String api = JIRA_HOST.concat(uri.getQuery()) + .concat("%20AND%20") + .concat("status=open"); + + httpRequest = HttpRequest.newBuilder() + .uri(new URI(api)) + .timeout(Duration.of(5, SECONDS)) // based on manual testing, timeout after 5 seconds works. + .GET() + .build(); + + HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + String jsonResponse = response.body(); + JsonNode jsonNode = objectMapper.readTree(jsonResponse); + + if (jsonNode.get("errorMessages") != null) { + LOGGER.error("Cannot request JIRA API for the plugin. {}", jsonNode.get("errorMessages")); + return Optional.empty(); + } + return Optional.of(jsonNode.get("total").asInt()); + } catch (JsonMappingException e) { + LOGGER.error("Cannot map JSON returned by JIRA API for the plugin. {}", e); + } catch (JsonProcessingException e) { + LOGGER.error("Cannot process JSON returned by JIRA API for the plugin. {}", e); + } catch (MalformedURLException e) { + LOGGER.error("Cannot process malformed URL for the plugin. {}", e); + } catch (URISyntaxException e) { + LOGGER.error("Incorrect URI syntax in the plugin. {}", e); + } catch (IOException e) { + LOGGER.error("Cannot read HttpResponse for the plugin. {}", e); + } catch (InterruptedException e) { + LOGGER.error("Interruption occurred when waiting for JIRA API for the plugin. {}", e); + } + return Optional.empty(); + } + + @Override + public String key() { + return KEY; + } + + @Override + public String getDescription() { + return "Returns total number of open issues in JIRA."; + } +} diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java index ba5e8694a..bec7ea5f8 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/probes/ProbeContext.java @@ -45,6 +45,7 @@ public class ProbeContext { private GitHub github; private ZonedDateTime lastCommitDate; private Map pluginDocumentationLinks; + private Map issueTrackerUrlsByNames; private Optional scmFolderPath; public ProbeContext(String pluginName, UpdateCenter updateCenter) throws IOException { @@ -76,19 +77,32 @@ public void setGitHub(GitHub github) { this.github = github; } - public void setPluginDocumentationLinks(Map pluginDocumentationLinks) { - this.pluginDocumentationLinks = pluginDocumentationLinks; - } - public Map getPluginDocumentationLinks() { return pluginDocumentationLinks; } + public void setPluginDocumentationLinks(Map pluginDocumentationLinks) { + this.pluginDocumentationLinks = pluginDocumentationLinks; + } + public Optional getRepositoryName(String scm) { final Matcher match = SCMLinkValidationProbe.GH_PATTERN.matcher(scm); return match.find() ? Optional.of(match.group("repo")) : Optional.empty(); } + public void setIssueTrackerNameAndUrl(Map issueTrackerNameAndUrl) { + this.issueTrackerUrlsByNames = issueTrackerNameAndUrl; + } + + /** + * Gets the issue tracker names and its urls. + * + * @return a Map that consists of {@link io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.IssueTrackers} "type" as key and "viewUrl" as its value. + */ + public Map getIssueTrackerUrlsByNames() { + return issueTrackerUrlsByNames; + } + public Optional getScmFolderPath() { return scmFolderPath; } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java index 540d488ce..681aa6039 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/CodeCoverageProbeTest.java @@ -112,7 +112,7 @@ public void shouldBeInErrorWhenRepositoryIsNotInOrganization() { when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -156,7 +156,7 @@ public void shouldBeSuccessfulWhenRetrievedDetailsFromGitHubChecksIsAboveMinimum when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -213,7 +213,7 @@ public void shouldBeSuccessfulWhenAboveThreshold() throws IOException { when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -270,7 +270,7 @@ public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnBothCr when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -327,10 +327,9 @@ public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnBranch when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, - "42", defaultBranch - ) + "42", defaultBranch) ), Map.of(), List.of() @@ -384,7 +383,7 @@ public void shouldFailWhenRetrievedDetailsFromGitHubChecksInBelowMinimumOnLine() when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -441,7 +440,7 @@ public void shouldBeInErrorIfThereIsNoCodeCoverage() throws IOException { when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java index 94f1c6b41..ac2f61f36 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/DeprecatedPluginProbeTest.java @@ -62,7 +62,7 @@ void shouldBeAbleToDetectNonDeprecatedPlugin() { when(plugin.getName()).thenReturn("foo"); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), + Map.of("foo", Plugin.of("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), Map.of("bar", new Deprecation("find-the-reason-here")), Collections.emptyList() )); @@ -80,7 +80,7 @@ void shouldBeAbleToDetectDeprecatedPlugin() { when(plugin.getName()).thenReturn("foo"); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main")), + Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now().minusDays(1), Collections.emptyList(), 0, "", "main", List.of())), Map.of("bar", new Deprecation("find-the-reason-here"), "foo", new Deprecation("this-is-the-reason")), Collections.emptyList() )); @@ -100,7 +100,7 @@ void shouldBeAbleToDetectDeprecatedPluginFromLabels() { when(plugin.getName()).thenReturn(pluginName); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new Plugin(pluginName, new VersionNumber("1.0"), "", ZonedDateTime.now(), List.of("deprecated"), 0, "2.361", "main") + pluginName, new Plugin(pluginName, new VersionNumber("1.0"), "", ZonedDateTime.now(), List.of("deprecated"), 0, "2.361", "main", List.of()) ), Map.of(), Collections.emptyList() diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/GitHubOpenIssuesProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/GitHubOpenIssuesProbeTest.java new file mode 100644 index 000000000..454162278 --- /dev/null +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/GitHubOpenIssuesProbeTest.java @@ -0,0 +1,182 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.IssueTrackers; +import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; + +import org.junit.jupiter.api.Test; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; + +class GitHubOpenIssuesProbeTest extends AbstractProbeTest { + @Override + GitHubOpenIssuesProbe getSpy() { + return spy(GitHubOpenIssuesProbe.class); + } + + @Test + void shouldNotRunWithInvalidProbeResultRequirement() { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + + when(plugin.getDetails()).thenReturn( + Map.of(), + Map.of( + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.failure(IssueTrackerDetectionProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.failure(IssueTrackerDetectionProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ) + ); + + final GitHubOpenIssuesProbe probe = getSpy(); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(GitHubOpenIssuesProbe.KEY, "github-open-issues does not meet the criteria to be executed on null")); + verify(probe, never()).doApply(plugin, ctx); + } + + @Test + void shouldBeAbleToFindNumberOfOpenIssuesInGH() throws IOException { + final String pluginName = "foo"; + final String repository = "jenkinsci/" + pluginName + "-plugin"; + final String scmLink = "https://github.com/" + repository; + + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + final GitHub gh = mock(GitHub.class); + final GHRepository ghRepository = mock(GHRepository.class); + final IssueTrackers issueTrackerGithub = new IssueTrackers("github", scmLink + "/issues", scmLink + "/issues/new/choose"); + + when(plugin.getName()).thenReturn(pluginName); + when(plugin.getScm()).thenReturn(scmLink); + when(plugin.getDetails()).thenReturn( + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ) + ); + + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerGithub) + )), + Map.of(), + List.of() + )); + + when(ctx.getIssueTrackerUrlsByNames()).thenReturn(Map.of("github", scmLink + "/issues")); + + when(ctx.getGitHub()).thenReturn(gh); + when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(repository)); + when(gh.getRepository(repository)).thenReturn(ghRepository); + when(ghRepository.getOpenIssueCount()).thenReturn(6); + + final GitHubOpenIssuesProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(GitHubOpenIssuesProbe.KEY, "6 open issues found in the foo plugin.")); + + verify(probe).doApply(plugin, ctx); + + } + + + @Test + void shouldFailWhereThereIsNoGitHubTracker() throws IOException { + final String pluginName = "foo"; + final String repository = "jenkinsci/" + pluginName + "-plugin"; + final String scmLink = "https://github.com/" + repository; + + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + final GitHub gh = mock(GitHub.class); + final GHRepository ghRepository = mock(GHRepository.class); + + when(plugin.getName()).thenReturn(pluginName); + when(plugin.getScm()).thenReturn(scmLink); + when(plugin.getDetails()).thenReturn( + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ) + ); + + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main" + )), + Map.of(), + List.of() + )); + + when(ctx.getIssueTrackerUrlsByNames()).thenReturn(Map.of()); + + when(ctx.getGitHub()).thenReturn(gh); + when(ctx.getRepositoryName(plugin.getScm())).thenReturn(Optional.of(repository)); + when(gh.getRepository(repository)).thenReturn(ghRepository); + + final GitHubOpenIssuesProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.failure(GitHubOpenIssuesProbe.KEY, "Could not find open issues in the foo plugin.")); + + verify(probe).doApply(plugin, ctx); + } + +} diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java index 985d0dd39..af2c14d77 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/InstallationStatProbeTest.java @@ -92,7 +92,7 @@ void shouldBeAbleToFindInstallationCountInUpdateCenter() { when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, - new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin(pluginName, null, null, null, List.of(), 100, "", "main") + io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of(pluginName, null, null, null, List.of(), 100, "", "main") ), Map.of(), List.of() diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IssueTrackerDetectionProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IssueTrackerDetectionProbeTest.java new file mode 100644 index 000000000..24f5370f7 --- /dev/null +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/IssueTrackerDetectionProbeTest.java @@ -0,0 +1,282 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.IssueTrackers; +import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; + +import org.junit.jupiter.api.Test; + +class IssueTrackerDetectionProbeTest extends AbstractProbeTest { + @Override + IssueTrackerDetectionProbe getSpy() { + return spy(IssueTrackerDetectionProbe.class); + } + + @Test + void shouldNotRunWithInvalidProbeResultRequirement() { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + + when(plugin.getDetails()).thenReturn( + Map.of(), + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.failure(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + final IssueTrackerDetectionProbe probe = getSpy(); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(IssueTrackerDetectionProbe.KEY, "issue-tracker-detection does not meet the criteria to be executed on null")); + verify(probe, never()).doApply(plugin, ctx); + } + + @Test + void shouldDetectIssueTrackersInPlugin() throws IOException { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final IssueTrackers issueTrackerGithub = new IssueTrackers("github", "https://github.com/foo-plugin/issues", "https://github.com/foo-plugin/issues/new/choose"); + final IssueTrackers issueTrackerJira = new IssueTrackers("jira", "https://issues.jenkins.io/issues/?jql=component=18331", "https://www.jenkins.io/participate/report-issue/redirect/#18331"); + final String pluginName = "foo"; + + when(plugin.getDetails()).thenReturn( + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerGithub, issueTrackerJira) + )), + Map.of(), + List.of() + )); + + final IssueTrackerDetectionProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(IssueTrackerDetectionProbe.KEY, "Found 2 issue trackers configured for the foo plugin.")); + + assertThat(ctx.getIssueTrackerUrlsByNames()).contains(entry("github", "https://github.com/foo-plugin/issues"), entry("jira", "https://issues.jenkins.io/issues/?jql=component=18331")); + verify(probe).doApply(plugin, ctx); + } + + @Test + void shouldAlwaysFilterDataForTheCorrectPluginFromIssueTrackers() throws IOException { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final IssueTrackers issueTrackerJira = new IssueTrackers("jira", "https://issues.jenkins.io/issues/?jql=component=18331", "https://www.jenkins.io/participate/report-issue/redirect/#18331"); + final String correctPluginToFilterFor = "foo"; + final String inCorrectPlugin = "bar"; + + when(plugin.getDetails()).thenReturn( + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + when(plugin.getName()).thenReturn(correctPluginToFilterFor); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(correctPluginToFilterFor, + new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + correctPluginToFilterFor, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerJira) + ), + inCorrectPlugin, + new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + inCorrectPlugin, null, null, null, List.of(), 0, "2.361.1", "main", + List.of()) + ), + Map.of(), + List.of() + )); + + Map correctIssueSetToMatch = new HashMap<>(); + correctIssueSetToMatch.put("jira", "https://issues.jenkins.io/issues/?jql=component=18331"); + + final IssueTrackerDetectionProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(IssueTrackerDetectionProbe.KEY, "Found 1 issue trackers configured for the foo plugin.")); + + assertThat(plugin.getName()).isEqualTo(correctPluginToFilterFor); + assertThat(ctx.getIssueTrackerUrlsByNames()).containsExactlyEntriesOf(correctIssueSetToMatch); + verify(probe).doApply(plugin, ctx); + } + + @Test + void shouldDetectForOnlyGHInIssueTrackers() throws IOException { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final IssueTrackers issueTrackerGithub = new IssueTrackers("github", "https://github.com/foo-plugin/issues", "https://github.com/foo-plugin/issues/new/choose"); + final String pluginName = "foo"; + + when(plugin.getDetails()).thenReturn( + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerGithub) + )), + Map.of(), + List.of() + )); + + final IssueTrackerDetectionProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(IssueTrackerDetectionProbe.KEY, "Found 1 issue trackers configured for the foo plugin.")); + + assertThat(ctx.getIssueTrackerUrlsByNames()).contains(entry("github", "https://github.com/foo-plugin/issues")); + verify(probe).doApply(plugin, ctx); + } + + + @Test + void shouldDetectForOnlyJIRAInIssueTrackers() throws IOException { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = spy(new ProbeContext(plugin.getName(), new UpdateCenter(Map.of(), Map.of(), List.of()))); + final IssueTrackers issueTrackerJira = new IssueTrackers("jira", "https://issues.jenkins.io/issues/?jql=component=18331", "https://www.jenkins.io/participate/report-issue/redirect/#18331"); + final String pluginName = "foo"; + + when(plugin.getDetails()).thenReturn( + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerJira) + )), + Map.of(), + List.of() + )); + + final IssueTrackerDetectionProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(IssueTrackerDetectionProbe.KEY, "Found 1 issue trackers configured for the foo plugin.")); + + assertThat(ctx.getIssueTrackerUrlsByNames()).contains(entry("jira", "https://issues.jenkins.io/issues/?jql=component=18331")); + verify(probe).doApply(plugin, ctx); + } + + @Test + void shouldFailWhenPluginIssueTrackersIsNotInUpdateCenter() { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + final String pluginName = "foo"; + + when(plugin.getDetails()).thenReturn( + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of() + )), + Map.of(), + List.of() + )); + + final IssueTrackerDetectionProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.failure(IssueTrackerDetectionProbe.KEY, "No issue tracker data available for foo plugin in Update Center.")); + + verify(probe).doApply(plugin, ctx); + } + + @Test + void shouldFailWhenPluginIsNotInUpdateCenter() { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + final String pluginName = "foo"; + + when(plugin.getDetails()).thenReturn( + Map.of( + UpdateCenterPluginPublicationProbe.KEY, ProbeResult.success(UpdateCenterPluginPublicationProbe.KEY, "") + ) + ); + + when(plugin.getName()).thenReturn(pluginName); + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(), + Map.of(), + List.of() + )); + + final IssueTrackerDetectionProbe probe = getSpy(); + + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.failure(IssueTrackerDetectionProbe.KEY, "No issue tracker data available for foo plugin in Update Center.")); + + verify(probe).doApply(plugin, ctx); + } +} diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java index 761e0a083..de2e22a34 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JenkinsCoreProbeTest.java @@ -94,7 +94,7 @@ void shouldBeAbleToExtractJenkinsVersionFromUpdateCenter() { when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( pluginName, - new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, null, null, null, List.of(), 0, "2.361.1", "main" ) ), diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JiraOpenIssuesProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JiraOpenIssuesProbeTest.java new file mode 100644 index 000000000..011efc393 --- /dev/null +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/JiraOpenIssuesProbeTest.java @@ -0,0 +1,193 @@ +/* + * MIT License + * + * Copyright (c) 2023 Jenkins Infra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.jenkins.pluginhealth.scoring.probes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; + +import io.jenkins.pluginhealth.scoring.model.Plugin; +import io.jenkins.pluginhealth.scoring.model.ProbeResult; +import io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.IssueTrackers; +import io.jenkins.pluginhealth.scoring.model.updatecenter.UpdateCenter; + +import org.junit.jupiter.api.Test; + +class JiraOpenIssuesProbeTest extends AbstractProbeTest { + @Override + JiraOpenIssuesProbe getSpy() { + return spy(JiraOpenIssuesProbe.class); + } + + @Test + void shouldNotRunWithInvalidProbeResultRequirement() { + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + + when(plugin.getDetails()).thenReturn( + Map.of(), + Map.of( + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.failure(IssueTrackerDetectionProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.failure(IssueTrackerDetectionProbe.KEY, "") + ), + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.failure(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ) + ); + + final JiraOpenIssuesProbe probe = getSpy(); + assertThat(probe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.error(JiraOpenIssuesProbe.KEY, "jira-open-issues does not meet the criteria to be executed on null")); + verify(probe, never()).doApply(plugin, ctx); + } + + @Test + void shouldBeAbleToFindNumberOfOpenIssuesInJira() throws IOException, InterruptedException { + final String pluginName = "mailer"; + final String repository = "jenkinsci/" + pluginName + "-plugin"; + final String scmLink = "https://github.com/" + repository; + + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + final HttpClient mockedHttpClient = mock(HttpClient.class); + final HttpResponse mockedHttpResponse = mock(HttpResponse.class); + + final IssueTrackers issueTrackerJira = + new IssueTrackers("jira", "https://issues.jenkins.io/issues/?jql=component=18331", "https://www.jenkins.io/participate/report-issue/redirect/#18331"); + + final String JSONString = "{\"expand\":\"names,schema\",\"startAt\":0,\"maxResults\":50,\"total\":10,\"issues\":[]}"; + + when(plugin.getDetails()).thenReturn( + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ) + ); + + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerJira) + )), + Map.of(), + List.of() + )); + + when(plugin.getName()).thenReturn(pluginName); + when(plugin.getScm()).thenReturn(scmLink); + when(ctx.getIssueTrackerUrlsByNames()).thenReturn(Map.of("jira", "https://issues.jenkins.io/issues/?jql=component=18331")); + + when(mockedHttpResponse.body()).thenReturn(JSONString); + + when(mockedHttpClient.send( + any(HttpRequest.class), + any(HttpResponse.BodyHandler.class) + )).thenReturn(mockedHttpResponse); + + final JiraOpenIssuesProbe jiraOpenIssuesProbe = spy(JiraOpenIssuesProbe.class); + jiraOpenIssuesProbe.httpClient = mockedHttpClient; + + assertThat(jiraOpenIssuesProbe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.success(JiraOpenIssuesProbe.KEY, "10 open issues found in the mailer plugin.")); + } + + @Test + void shouldReturnErrorWhenJIRAReturnsErrors() throws IOException, InterruptedException { + final String pluginName = "foo"; + final String repository = "jenkinsci/" + pluginName + "-plugin"; + final String scmLink = "https://github.com/" + repository; + + final Plugin plugin = mock(Plugin.class); + final ProbeContext ctx = mock(ProbeContext.class); + final HttpClient mockedHttpClient = mock(HttpClient.class); + final HttpResponse mockedHttpResponse = mock(HttpResponse.class); + + final IssueTrackers issueTrackerJira = + new IssueTrackers("jira", "https://issues.jenkins.io/issues/?jql=component=0", "https://www.jenkins.io/participate/report-issue/redirect/#0"); + + final String errorJSONString = "{\"errorMessages\":[\"A value with ID '0' does not exist for the field 'component'.\"],\"errors\":{}}"; + + when(plugin.getDetails()).thenReturn( + Map.of( + SCMLinkValidationProbe.KEY, ProbeResult.success(SCMLinkValidationProbe.KEY, ""), + IssueTrackerDetectionProbe.KEY, ProbeResult.success(IssueTrackerDetectionProbe.KEY, "") + ) + ); + + when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( + Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, null, null, null, List.of(), 0, "2.361.1", "main", + List.of(issueTrackerJira) + )), + Map.of(), + List.of() + )); + + when(plugin.getName()).thenReturn(pluginName); + when(plugin.getScm()).thenReturn(scmLink); + when(ctx.getIssueTrackerUrlsByNames()).thenReturn(Map.of("jira", "https://issues.jenkins.io/issues/?jql=component=0")); + + when(mockedHttpResponse.body()).thenReturn(errorJSONString); + + when(mockedHttpClient.send( + any(HttpRequest.class), + any(HttpResponse.BodyHandler.class) + )).thenReturn(mockedHttpResponse); + + final JiraOpenIssuesProbe jiraOpenIssuesProbe = spy(JiraOpenIssuesProbe.class); + jiraOpenIssuesProbe.httpClient = mockedHttpClient; + + assertThat(jiraOpenIssuesProbe.apply(plugin, ctx)) + .usingRecursiveComparison() + .comparingOnlyFields("id", "status", "message") + .isEqualTo(ProbeResult.failure(JiraOpenIssuesProbe.KEY, "Could not find open issues in the foo plugin.")); + } +} diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java index 90cf1e042..f4eec51db 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/KnownSecurityVulnerabilityProbeTest.java @@ -78,7 +78,7 @@ void shouldBeOKWithNoSecurityWarning() { void shouldBeOKWithWarningOnDifferentPlugin() { final String pluginName = "foo-bar"; final VersionNumber pluginVersion = new VersionNumber("1.0"); - final var pluginInUC = new Plugin(pluginName, pluginVersion, "scm", ZonedDateTime.now().minusHours(1), List.of(), 0, "", "main"); + final var pluginInUC = Plugin.of(pluginName, pluginVersion, "scm", ZonedDateTime.now().minusHours(1), List.of(), 0, "", "main"); final var plugin = mock(io.jenkins.pluginhealth.scoring.model.Plugin.class); final ProbeContext ctx = mock(ProbeContext.class); final KnownSecurityVulnerabilityProbe probe = getSpy(); diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java index 241bf51f3..56bb75b79 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/SpotBugsProbeTest.java @@ -85,7 +85,7 @@ public void shouldFailWhenRepositoryIsNotInOrganization() { when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -127,7 +127,7 @@ public void shouldBeAbleToRetrieveDetailsFromGitHubChecks() throws IOException { when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) @@ -179,7 +179,7 @@ public void shouldFailIfThereIsNoSpotBugs() throws IOException { when(plugin.getScm()).thenReturn(scmLink); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( Map.of( - pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, new VersionNumber("1.0"), scmLink, ZonedDateTime.now(), List.of(), 0, "42", defaultBranch ) diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java index dc673db6d..50659cd96 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpForAdoptionProbeTest.java @@ -61,7 +61,7 @@ void shouldBeAbleToDetectPluginForAdoption() { when(plugin.getName()).thenReturn("foo"); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "not-a-scm", ZonedDateTime.now().minusDays(1), List.of("builder", "adopt-this-plugin"), 0, "", "main")), + Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "not-a-scm", ZonedDateTime.now().minusDays(1), List.of("builder", "adopt-this-plugin"), 0, "", "main", List.of())), Collections.emptyMap(), Collections.emptyList() )); @@ -79,7 +79,7 @@ void shouldBeAbleToDetectPluginNotForAdoption() { when(plugin.getName()).thenReturn("foo"); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of("foo", new Plugin("foo", new VersionNumber("1.0"), "not-a-scm", ZonedDateTime.now().minusDays(1), List.of("builder"), 0, "", "main")), + Map.of("foo", Plugin.of("foo", new VersionNumber("1.0"), "not-a-scm", ZonedDateTime.now().minusDays(1), List.of("builder"), 0, "", "main")), Collections.emptyMap(), Collections.emptyList() )); diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java index 59ff0bd49..2c9d4fdfb 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/probes/UpdateCenterPluginPublicationProbeTest.java @@ -80,7 +80,7 @@ void shouldSucceedIfPluginIsInUpdateCenterMap() { when(plugin.getName()).thenReturn(pluginName); when(ctx.getUpdateCenter()).thenReturn(new UpdateCenter( - Map.of(pluginName, new io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin( + Map.of(pluginName, io.jenkins.pluginhealth.scoring.model.updatecenter.Plugin.of( pluginName, null, null, null, List.of(), 0, "2.361.1", "main" )), Map.of(),