diff --git a/.env b/.env index 608c4cdd6..edc7c17aa 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ # The Sonarqube base image. 'latest' if building locally, '8.5-community' if targeting a specific version -SONARQUBE_VERSION=latest +SONARQUBE_VERSION=9.5-community # The name of the Dockerfile to run. 'Dockerfile' is building locally, 'release.Dockerfile' if building the release image -DOCKERFILE=Dockerfile +DOCKERFILE=release.Dockerfile # The version of the plugin to include in the image -PLUGIN_VERSION=1.10.0-SNAPSHOT +PLUGIN_VERSION=1.12.0 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..5cca5584d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [mc1arke] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 370403ba0..b3adb2fc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Init id: init @@ -45,17 +45,17 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Java ${{ matrix.java }} - uses: actions/setup-java@v2.3.1 + uses: actions/setup-java@v3 with: java-version: ${{ matrix.java }} java-package: jdk distribution: 'zulu' - name: Cache deps - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} @@ -68,7 +68,7 @@ jobs: - name: Archive artifact if: success() && matrix.java == '11' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: snapshot path: build/libs/*.jar @@ -80,19 +80,19 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Java - uses: actions/setup-java@v2.3.1 + uses: actions/setup-java@v3 with: java-version: 11 java-package: jdk distribution: 'zulu' - name: Cache deps - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} @@ -118,7 +118,7 @@ jobs: - name: Archive artifact if: success() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: release path: build/libs/*.jar @@ -141,19 +141,19 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Java - uses: actions/setup-java@v2.3.1 + uses: actions/setup-java@v3 with: java-version: 11 java-package: jdk distribution: 'zulu' - name: Cache deps - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4912202ac..3e4f01a73 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,15 +24,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/Dockerfile b/Dockerfile index 9c3de7a6f..a5ef7eff4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ ARG SONARQUBE_VERSION -FROM openjdk:11-jdk-slim as builder +FROM gradle:7.3.3-jdk11-alpine as builder COPY . /home/build/project WORKDIR /home/build/project -RUN ./gradlew build -x test +RUN gradle build -x test FROM sonarqube:${SONARQUBE_VERSION} COPY --from=builder --chown=sonarqube:sonarqube /home/build/project/build/libs/sonarqube-community-branch-plugin-*.jar /opt/sonarqube/extensions/plugins/ diff --git a/README.md b/README.md index f5ec79c37..ca3268bc6 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,29 @@ [![Build Status](https://img.shields.io/github/workflow/status/mc1arke/sonarqube-community-branch-plugin/build?label=build&logo=github)](https://github.com/mc1arke/sonarqube-community-branch-plugin?workflow=build) # Sonarqube Community Branch Plugin + A plugin for SonarQube to allow branch analysis in the Community version. # Support -This plugin is not maintained or supported by SonarSource and has no official upgrade path for migrating from the SonarQube Community Edition to any of the Commercial Editions (Developer, Enterprise, or Data Center Edition). Support for any problems is only available through issues on the Github repository or through alternative channels (e.g. StackOverflow) and any attempt to request support for this plugin directly from SonarSource or an affiliated channel (e.g. Sonar Community forum) is likely to result in your request being closed or ignored. -If you plan on migrating your SonarQube data to a commercial edition after using this plugin then please be aware that this may result in some or all of your data being lost due to this compatibility of this plugin and the official SonarQube branch features being untested. +This plugin is not maintained or supported by SonarSource and has no official upgrade path for migrating from the +SonarQube Community Edition to any of the Commercial Editions (Developer, Enterprise, or Data Center Edition). Support +for any problems is only available through issues on the Github repository or through alternative channels (e.g. +StackOverflow) and any attempt to request support for this plugin directly from SonarSource or an affiliated channel ( +e.g. Sonar Community forum) is likely to result in your request being closed or ignored. + +If you plan on migrating your SonarQube data to a commercial edition after using this plugin then please be aware that +this may result in some or all of your data being lost due to this compatibility of this plugin and the official +SonarQube branch features being untested. # Compatibility + Use the following table to find the correct plugin version for each SonarQube version SonarQube Version | Plugin Version ------------------|--------------- -9.0+ | 1.9.0 +9.1+ | 1.11.0 +9.0 | 1.9.0 8.9 | 1.8.1 8.7 - 8.8 | 1.7.0 8.5 - 8.6 | 1.6.0 @@ -24,44 +34,100 @@ SonarQube Version | Plugin Version 7.4 - 7.7 | 1.0.2 # Features -The plugin is intended to support the [features and parameters specified in the SonarQube documentation](https://docs.sonarqube.org/latest/branches/overview/). + +The plugin is intended to support the +[features and parameters from the SonarQube documentation](https://docs.sonarqube.org/latest/branches/overview/). # Installation ## Manual Install -__Please ensure you follow the installation instructions for the version of the plugin you're installing by looking at the README on the relevant release tag.__ -Either build the project or [download a compatible release version of the plugin JAR](https://github.com/mc1arke/sonarqube-community-branch-plugin/releases). +__Please ensure you follow the installation instructions for the version of the plugin you're installing by looking at +the README on the relevant release tag.__ + +Either build the project +or [download a compatible release version of the plugin JAR](https://github.com/mc1arke/sonarqube-community-branch-plugin/releases) +. 1. Copy the plugin JAR file to the `extensions/plugins/` directory of your SonarQube instance -2. Add `-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${version}.jar=web` to the `sonar.web.javaAdditionalOpts` property in your Sonarqube installation's `config/sonar.properties` file, e.g. `sonar.web.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.0.jar=web` -3. Add `-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${version}.jar=ce` to the `sonar.ce.javaAdditionalOpts` property in your Sonarqube installation's `config/sonar.properties` file, e.g. `sonar.ce.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.0.jar=ce` +2. Add `-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${version}.jar=web` to + the `sonar.web.javaAdditionalOpts` property in your Sonarqube installation's `conf/sonar.properties` file, + e.g. `sonar.web.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.0.jar=web` +3. Add `-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${version}.jar=ce` to + the `sonar.ce.javaAdditionalOpts` property in your Sonarqube installation's `conf/sonar.properties` file, + e.g. `sonar.ce.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.0.jar=ce` 4. Start Sonarqube, and accept the warning about using third-party plugins ## Docker -The plugin is distributed in the [mc1arke/sonarqube-with-community-branch-plugin](https://hub.docker.com/r/mc1arke/sonarqube-with-community-branch-plugin) Docker image, with the image versions matching the up-stream Sonarqube image version. -__Note:__ If you're setting the `SONAR_WEB_JAVAADDITIONALOPTS` or `SONAR_CE_JAVAADDITIONALOPTS` environment variables in your container launch then you'll need to add the `javaagent` configuration to your overrides to match what's in the provided Dockerfile. +The plugin is distributed in +the [mc1arke/sonarqube-with-community-branch-plugin](https://hub.docker.com/r/mc1arke/sonarqube-with-community-branch-plugin) +Docker image, with the image versions matching the up-stream Sonarqube image version. + +__Note:__ If you're setting the `SONAR_WEB_JAVAADDITIONALOPTS` or `SONAR_CE_JAVAADDITIONALOPTS` environment variables in +your container launch then you'll need to add the `javaagent` configuration to your overrides to match what's in the +provided Dockerfile. + +## Kubernetes with official Helm Chart + +When using +[Sonarqube official Helm Chart](https://github.com/SonarSource/helm-chart-sonarqube/tree/master/charts/sonarqube), +you need to add the following settings to your helm values, where `${version}` should be replaced with the plugin +version (e.g. `1.11.0`): + +```yaml +plugins: + install: + - https://github.com/mc1arke/sonarqube-community-branch-plugin/releases/download/${version}/sonarqube-community-branch-plugin-${version}.jar + lib: + - sonarqube-community-branch-plugin-${version}.jar +jvmOpts: "-javaagent:/opt/sonarqube/lib/common/sonarqube-community-branch-plugin-${version}.jar=web" +jvmCeOpts: "-javaagent:/opt/sonarqube/lib/common/sonarqube-community-branch-plugin-${version}.jar=ce" +``` + +### Issues with file path with persistency + +If you set `persistence.enabled=true` on SonarQube chart, the plugin might be copied to this path: + +``` +/opt/sonarqube/lib/common/sonarqube-community-branch-plugin-${version}.jar/sonarqube-community-branch-plugin-${version}.jar +``` + +instead of this: + +``` +/opt/sonarqube/lib/common/sonarqube-community-branch-plugin-${version}.jar +``` + +As a workaround either change the paths in the config above, or exec into the container and move file up the directory +to match the config. # Configuration + ## Global configuration + Make sure `sonar.core.serverBaseURL` in SonarQube [/admin/settings](http://localhost:9000/admin/settings) is properly - set in order to for the links in the comment to work. +set in order to for the links in the comment to work. Set all other properties that you can define globally for all of your projects. -## How to decorate the PR +## How to decorate a Pull Request + In order to decorate your Pull Request's source branch, you need to analyze your target branch first. ### Run analysis of branches - -The analysis needs the following setting: + +If the scan is being run from a CI supporting auto-configuration then the scanner can be launched without any branch +parameters. Otherwise, the analysis needs the following setting: `sonar.branch.name = branch_name (e.g master)` ### Run analysis of the PR branch -Carefully read the official SonarQube guide for [pull request decoration](https://docs.sonarqube.org/latest/analysis/pull-request/) -In there you'll find the following properties that need to be set. +Carefully read the official SonarQube guide +for [pull request decoration](https://docs.sonarqube.org/latest/analysis/pull-request/) + +In there you'll find the following properties that need to be set, unless your CI support auto-configuration. + ``` sonar.pullrequest.key = pull_request_id (e.g. 100) sonar.pullrequest.branch = source_branch_name (e.g feature/TICKET-123) @@ -69,16 +135,22 @@ sonar.pullrequest.base = target_branch_name (e.g master) ``` :warning: There must not be any `sonar.branch` properties like `sonar.branch.name` arguments set when you analyze a - pull-request. These properties indicate to sonar that a branch is being analyzed rather than a pull-request so no - pull-request decoration will be executed. - +pull-request. These properties indicate to sonar that a branch is being analyzed rather than a pull-request so no +pull-request decoration will be executed. + ## Serving images for PR decoration -By default, images for PR decoration are served as static resources on the SonarQube server as a part of Community Branch Plugin. -If you use a SonarQube server behind a firewall and/or PR service (Github, Gitlab etc.) hasn't access to SonarQube server, you should change `Images base URL` property in `General > Pull Request` settings. +By default, images for PR decoration are served as static resources on the SonarQube server as a part of Community +Branch Plugin. + +If you use a SonarQube server behind a firewall and/or PR service (Github, Gitlab etc.) doesn't have access to SonarQube +server, you should change `Images base URL` property in `General > Pull Request` settings. + +Anyone needing to set this value can use the +URL `https://raw.githubusercontent.com/mc1arke/sonarqube-community-branch-plugin/master/src/main/resources/static`, or +download the files from this location and host them themself. -Anyone needing to set this value can use the URL `https://raw.githubusercontent.com/mc1arke/sonarqube-community-branch-plugin/master/src/main/resources/static`, or download the files from this location and host them themself. - # Building the plugin from source -In case you want to try and test the current branch or build it for your development execute `./gradlew clean build -` inside of the project directory. This will put the built jar under `libs/sonarqube-community-branch-plugin*.jar` + +If you want to try and test the current branch or build it for your development execute `./gradlew clean build` +inside of the project directory. This will put the built jar under `libs/sonarqube-community-branch-plugin*.jar` diff --git a/build.gradle b/build.gradle index c913aeeef..5b02091a0 100644 --- a/build.gradle +++ b/build.gradle @@ -52,23 +52,27 @@ configurations { compileJava { options.compilerArgs += '-proc:none' + options.encoding = 'UTF-8' +} +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' } dependencies { compileOnly(fileTree(dir: sonarLibraries, include: '**/*.jar', exclude: 'extensions/*.jar')) testImplementation(fileTree(dir: sonarLibraries, include: '**/*.jar', exclude: 'extensions/*.jar')) - testImplementation('org.mockito:mockito-core:4.0.0') - testImplementation('org.assertj:assertj-core:3.21.0') + testImplementation('org.mockito:mockito-core:4.6.1') + testImplementation('org.assertj:assertj-core:3.23.1') testImplementation('com.github.tomakehurst:wiremock:2.27.2') zip("sonarqube:sonarqube:${sonarqubeVersion}@zip") - implementation('org.bouncycastle:bcpkix-jdk15on:1.69') + implementation('org.bouncycastle:bcpkix-jdk15on:1.70') implementation(files('lib/nodes-0.5.0.jar')) - runtimeOnly('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.0') + runtimeOnly('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3') compileOnly('com.google.code.findbugs:jsr305:3.0.2') - implementation('org.javassist:javassist:3.28.0-GA') - implementation('com.squareup.okhttp3:logging-interceptor:4.9.2') - testImplementation(platform('org.junit:junit-bom:5.8.1')) + implementation('org.javassist:javassist:3.29.0-GA') + implementation('com.squareup.okhttp3:logging-interceptor:4.10.0') + testImplementation(platform('org.junit:junit-bom:5.8.2')) testImplementation('org.junit.jupiter:junit-jupiter') testImplementation('junit:junit:4.13.2') testRuntimeOnly('org.junit.vintage:junit-vintage-engine') diff --git a/gradle.properties b/gradle.properties index 5dfdd5612..bb2ce9ff3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.10.0-SNAPSHOT +version=1.12.1 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index af7be50b1..669386b87 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882ed..1b6c78733 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java index 12e3a1a0a..e464feaa6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,15 +21,26 @@ import com.github.mc1arke.sonarqube.plugin.almclient.DefaultLinkHeaderReader; import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.DefaultBitbucketClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.HttpClientBuilderFactory; import com.github.mc1arke.sonarqube.plugin.almclient.github.DefaultGithubClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.DefaultUrlConnectionProvider; import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.DefaultGraphqlProvider; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory; import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchParamsValidator; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectBranchesLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectPullRequestsLoader; import com.github.mc1arke.sonarqube.plugin.scanner.ScannerPullRequestPropertySensor; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.AzureDevopsAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.BitbucketPipelinesAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.CirrusCiAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.CodeMagicAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.GithubActionsAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.GitlabCiAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.JenkinsAutoConfigurer; import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchFeatureExtension; import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchSupportDelegate; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.validator.AzureDevopsValidator; @@ -79,9 +90,12 @@ public void load(CoreExtension.Context context) { ValidateBindingAction.class, GithubValidator.class, + DefaultGraphqlProvider.class, DefaultGithubClientFactory.class, DefaultLinkHeaderReader.class, + DefaultUrlConnectionProvider.class, RestApplicationAuthenticationProvider.class, + HttpClientBuilderFactory.class, DefaultBitbucketClientFactory.class, BitbucketValidator.class, GitlabValidator.class, @@ -143,7 +157,11 @@ public void define(Plugin.Context context) { if (SonarQubeSide.SCANNER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(CommunityProjectBranchesLoader.class, CommunityProjectPullRequestsLoader.class, CommunityBranchConfigurationLoader.class, CommunityBranchParamsValidator.class, - ScannerPullRequestPropertySensor.class); + ScannerPullRequestPropertySensor.class, BranchConfigurationFactory.class, + AzureDevopsAutoConfigurer.class, BitbucketPipelinesAutoConfigurer.class, + CirrusCiAutoConfigurer.class, CodeMagicAutoConfigurer.class, + GithubActionsAutoConfigurer.class, GitlabCiAutoConfigurer.class, + JenkinsAutoConfigurer.class); } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketClient.java index 1c310e4f9..336df3ba1 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketClient.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke + * Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,10 +23,8 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository; -import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.time.Instant; @@ -55,21 +53,21 @@ public interface BitbucketClient { */ CodeInsightsReport createCodeInsightsReport(List reportData, String reportDescription, Instant creationDate, String dashboardUrl, - String logoUrl, QualityGate.Status status); + String logoUrl, ReportStatus reportStatus); /** * Deletes all code insights annotations for the given parameters. * * @throws IOException if the annotations cannot be deleted */ - void deleteAnnotations(String project, String repo, String commitSha) throws IOException; + void deleteAnnotations(String commitSha, String reportKey) throws IOException; /** * Uploads CodeInsights Annotations for the given commit. * * @throws IOException if the annotations cannot be uploaded */ - void uploadAnnotations(String project, String repo, String commitSha, Set annotations) throws IOException; + void uploadAnnotations(String commitSha, Set annotations, String reportKey) throws IOException; /** * Creates a DataValue of type DataValue.Link or DataValue.CloudLink depending on the implementation @@ -79,7 +77,7 @@ CodeInsightsReport createCodeInsightsReport(List reportData, /** * Uploads the code insights report for the given commit */ - void uploadReport(String project, String repo, String commitSha, CodeInsightsReport codeInsightReport) throws IOException; + void uploadReport(String commitSha, CodeInsightsReport codeInsightReport, String reportKey) throws IOException; /** *

@@ -104,32 +102,10 @@ CodeInsightsReport createCodeInsightsReport(List reportData, */ AnnotationUploadLimit getAnnotationUploadLimit(); - /** - * Extract the name of the project from the relevant configuration. The project is - * the value that should be used in the calls that take a `project` parameter. - * - * @param almSettingDto the global `AlmSettingDto` containing the global configuration for this ALM - * @param projectAlmSettingDto the `ProjectAlmSettingDto` assigned to the current project - * @return the resolved project name. - */ - String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto); - - /** - * Extract the name of the repository from the relevant configuration. The project is - * the value that should be used in the calls that take a `repository` parameter. - * - * @param almSettingDto the global `AlmSettingDto` containing the global configuration for this ALM - * @param projectAlmSettingDto the `ProjectAlmSettingDto` assigned to the current project - * @return the resolved repository name. - */ - String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto); - /** * Retrieve the details of the repository from the target Bitbucket instance. - * @param project the project as resolved from {@link #resolveProject(AlmSettingDto, ProjectAlmSettingDto)} - * @param repo the repository as resolved from {@link #resolveRepository(AlmSettingDto, ProjectAlmSettingDto)} * @return the repository details retrieved from Bitbucket. */ - Repository retrieveRepository(String project, String repo) throws IOException; + Repository retrieveRepository() throws IOException; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClient.java index c3e68d661..3d427b917 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClient.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke + * Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,12 +21,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository; -import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.BitbucketCloudConfiguration; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudAnnotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudCreateReportRequest; import okhttp3.MediaType; @@ -34,11 +35,8 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -52,11 +50,9 @@ import static java.lang.String.format; - class BitbucketCloudClient implements BitbucketClient { private static final Logger LOGGER = Loggers.get(BitbucketCloudClient.class); - private static final String REPORT_KEY = "com.github.mc1arke.sonarqube"; private static final MediaType APPLICATION_JSON_MEDIA_TYPE = MediaType.get("application/json"); private static final String TITLE = "SonarQube"; private static final String REPORTER = "SonarQube"; @@ -64,27 +60,25 @@ class BitbucketCloudClient implements BitbucketClient { private final ObjectMapper objectMapper; private final OkHttpClient okHttpClient; + private final BitbucketConfiguration bitbucketConfiguration; - BitbucketCloudClient(BitbucketCloudConfiguration config, ObjectMapper objectMapper, OkHttpClient.Builder baseClientBuilder) { - this(objectMapper, createAuthorisingClient(baseClientBuilder, negotiateBearerToken(config, objectMapper, baseClientBuilder.build()))); - } - BitbucketCloudClient(ObjectMapper objectMapper, OkHttpClient okHttpClient) { + BitbucketCloudClient(ObjectMapper objectMapper, OkHttpClient okHttpClient, BitbucketConfiguration bitbucketConfiguration) { this.objectMapper = objectMapper; this.okHttpClient = okHttpClient; + this.bitbucketConfiguration = bitbucketConfiguration; } - private static String negotiateBearerToken(BitbucketCloudConfiguration bitbucketCloudConfiguration, ObjectMapper objectMapper, OkHttpClient okHttpClient) { + static String negotiateBearerToken(String clientId, String clientSecret, ObjectMapper objectMapper, OkHttpClient okHttpClient) { Request request = new Request.Builder() - .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((bitbucketCloudConfiguration.getClientId() + ":" + bitbucketCloudConfiguration.getSecret()).getBytes( - StandardCharsets.UTF_8))) + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8))) .url("https://bitbucket.org/site/oauth2/access_token") .post(RequestBody.create("grant_type=client_credentials", MediaType.parse("application/x-www-form-urlencoded"))) .build(); try (Response response = okHttpClient.newCall(request).execute()) { - AuthToken authToken = objectMapper.readValue( - Optional.ofNullable(response.body()).orElseThrow(() -> new IllegalStateException("No response returned by Bitbucket Oauth")).string(), AuthToken.class); + BitbucketCloudClient.AuthToken authToken = objectMapper.readValue( + Optional.ofNullable(response.body()).orElseThrow(() -> new IllegalStateException("No response returned by Bitbucket Oauth")).string(), BitbucketCloudClient.AuthToken.class); return authToken.getAccessToken(); } catch (IOException ex) { throw new IllegalStateException("Could not retrieve bearer token", ex); @@ -106,7 +100,7 @@ public CodeInsightsAnnotation createCodeInsightsAnnotation(String issueKey, int @Override public CodeInsightsReport createCodeInsightsReport(List reportData, String reportDescription, Instant creationDate, String dashboardUrl, String logoUrl, - QualityGate.Status status) { + ReportStatus status) { return new CloudCreateReportRequest( reportData, reportDescription, @@ -116,16 +110,17 @@ public CodeInsightsReport createCodeInsightsReport(List reportData, dashboardUrl, // you need to change this to a real https URL for local debugging since localhost will get declined by the API logoUrl, "COVERAGE", - QualityGate.Status.ERROR.equals(status) ? "FAILED" : "PASSED" + ReportStatus.FAILED == status ? "FAILED" : "PASSED" ); } @Override - public void deleteAnnotations(String project, String repo, String commitSha) { + public void deleteAnnotations(String commitSha, String reportKey) { // not needed here. } - public void uploadAnnotations(String project, String repository, String commit, Set baseAnnotations) throws IOException { + @Override + public void uploadAnnotations(String commit, Set baseAnnotations, String reportKey) throws IOException { Set annotations = baseAnnotations.stream().map(CloudAnnotation.class::cast).collect(Collectors.toSet()); if (annotations.isEmpty()) { @@ -134,7 +129,7 @@ public void uploadAnnotations(String project, String repository, String commit, Request req = new Request.Builder() .post(RequestBody.create(objectMapper.writeValueAsString(annotations), APPLICATION_JSON_MEDIA_TYPE)) - .url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", project, repository, commit, REPORT_KEY)) + .url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s/annotations", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, reportKey)) .build(); LOGGER.info("Creating annotations on bitbucket cloud"); @@ -151,10 +146,10 @@ public DataValue createLinkDataValue(String dashboardUrl) { } @Override - public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException { - deleteExistingReport(project, repository, commit); + public void uploadReport(String commit, CodeInsightsReport codeInsightReport, String reportKey) throws IOException { + deleteExistingReport(commit, reportKey); - String targetUrl = format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", project, repository, commit, REPORT_KEY); + String targetUrl = format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, reportKey); String body = objectMapper.writeValueAsString(codeInsightReport); Request req = new Request.Builder() .put(RequestBody.create(body, APPLICATION_JSON_MEDIA_TYPE)) @@ -180,20 +175,10 @@ public AnnotationUploadLimit getAnnotationUploadLimit() { } @Override - public String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { - return almSettingDto.getAppId(); - } - - @Override - public String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { - return projectAlmSettingDto.getAlmRepo(); - } - - @Override - public Repository retrieveRepository(String project, String repo) throws IOException { + public Repository retrieveRepository() throws IOException { Request req = new Request.Builder() .get() - .url(format("https://api.bitbucket.org/2.0/repositories/%s/%s", project, repo)) + .url(format("https://api.bitbucket.org/2.0/repositories/%s/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository())) .build(); try (Response response = okHttpClient.newCall(req).execute()) { validate(response); @@ -205,10 +190,10 @@ public Repository retrieveRepository(String project, String repo) throws IOExcep } } - void deleteExistingReport(String project, String repository, String commit) throws IOException { + void deleteExistingReport(String commit, String reportKey) throws IOException { Request req = new Request.Builder() .delete() - .url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", project, repository, commit, REPORT_KEY)) + .url(format("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/reports/%s", bitbucketConfiguration.getProject(), bitbucketConfiguration.getRepository(), commit, reportKey)) .build(); LOGGER.info("Deleting existing reports on bitbucket cloud"); @@ -218,17 +203,6 @@ void deleteExistingReport(String project, String repository, String commit) thro } } - private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder baseClientBuilder, String bearerToken) { - return baseClientBuilder.addInterceptor(chain -> { - Request newRequest = chain.request().newBuilder() - .addHeader("Authorization", format("Bearer %s", bearerToken)) - .addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString()) - .build(); - return chain.proceed(newRequest); - }) - .build(); - } - void validate(Response response) { if (!response.isSuccessful()) { String error = Optional.ofNullable(response.body()).map(b -> { @@ -242,7 +216,7 @@ void validate(Response response) { } } - static class AuthToken { + private static class AuthToken { private final String accessToken; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClient.java index 47979b0fa..1ce79ae60 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClient.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Mathias Åhsberg, Michael Clarke + * Copyright (C) 2020-2022 Mathias Åhsberg, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,6 +24,7 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.Repository; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.Annotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.BitbucketServerConfiguration; @@ -36,11 +37,8 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.time.Instant; @@ -53,7 +51,6 @@ class BitbucketServerClient implements BitbucketClient { private static final Logger LOGGER = Loggers.get(BitbucketServerClient.class); - private static final String REPORT_KEY = "com.github.mc1arke.sonarqube"; private static final MediaType APPLICATION_JSON_MEDIA_TYPE = MediaType.get("application/json"); private static final String TITLE = "SonarQube"; private static final String REPORTER = "SonarQube"; @@ -63,10 +60,6 @@ class BitbucketServerClient implements BitbucketClient { private final ObjectMapper objectMapper; private final OkHttpClient okHttpClient; - BitbucketServerClient(BitbucketServerConfiguration config, ObjectMapper objectMapper, OkHttpClient.Builder baseClientBuilder) { - this(config, objectMapper, createAuthorisingClient(baseClientBuilder, config)); - } - BitbucketServerClient(BitbucketServerConfiguration config, ObjectMapper objectMapper, OkHttpClient okHttpClient) { this.config = config; this.objectMapper = objectMapper; @@ -85,7 +78,7 @@ public CodeInsightsAnnotation createCodeInsightsAnnotation(String issueKey, int } @Override - public CodeInsightsReport createCodeInsightsReport(List reportData, String reportDescription, Instant creationDate, String dashboardUrl, String logoUrl, QualityGate.Status status) { + public CodeInsightsReport createCodeInsightsReport(List reportData, String reportDescription, Instant creationDate, String dashboardUrl, String logoUrl, ReportStatus status) { return new CreateReportRequest( reportData, reportDescription, @@ -94,14 +87,15 @@ public CodeInsightsReport createCodeInsightsReport(List reportData, creationDate, dashboardUrl, logoUrl, - QualityGate.Status.ERROR.equals(status) ? "FAIL" : "PASS" + ReportStatus.FAILED == status ? "FAIL" : "PASS" ); } - public void deleteAnnotations(String project, String repository, String commit) throws IOException { + @Override + public void deleteAnnotations(String commit, String reportKey) throws IOException { Request req = new Request.Builder() .delete() - .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY)) + .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), config.getProject(), config.getRepository(), commit, reportKey)) .build(); try (Response response = okHttpClient.newCall(req).execute()) { validate(response); @@ -109,7 +103,7 @@ public void deleteAnnotations(String project, String repository, String commit) } @Override - public void uploadAnnotations(String project, String repository, String commit, Set annotations) throws IOException { + public void uploadAnnotations(String commit, Set annotations, String reportKey) throws IOException { if (annotations.isEmpty()) { return; } @@ -117,7 +111,7 @@ public void uploadAnnotations(String project, String repository, String commit, CreateAnnotationsRequest request = new CreateAnnotationsRequest(annotationSet); Request req = new Request.Builder() .post(RequestBody.create(objectMapper.writeValueAsString(request), APPLICATION_JSON_MEDIA_TYPE)) - .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), project, repository, commit, REPORT_KEY)) + .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s/annotations", config.getUrl(), config.getProject(), config.getRepository(), commit, reportKey)) .build(); try (Response response = okHttpClient.newCall(req).execute()) { validate(response); @@ -130,11 +124,11 @@ public DataValue createLinkDataValue(String dashboardUrl) { } @Override - public void uploadReport(String project, String repository, String commit, CodeInsightsReport codeInsightReport) throws IOException { + public void uploadReport(String commit, CodeInsightsReport codeInsightReport, String reportKey) throws IOException { String body = objectMapper.writeValueAsString(codeInsightReport); Request req = new Request.Builder() .put(RequestBody.create(body, APPLICATION_JSON_MEDIA_TYPE)) - .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", config.getUrl(), project, repository, commit, REPORT_KEY)) + .url(format("%s/rest/insights/1.0/projects/%s/repos/%s/commits/%s/reports/%s", config.getUrl(), config.getProject(), config.getRepository(), commit, reportKey)) .build(); try (Response response = okHttpClient.newCall(req).execute()) { @@ -166,20 +160,10 @@ public AnnotationUploadLimit getAnnotationUploadLimit() { } @Override - public String resolveProject(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { - return projectAlmSettingDto.getAlmRepo(); - } - - @Override - public String resolveRepository(AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { - return projectAlmSettingDto.getAlmSlug(); - } - - @Override - public Repository retrieveRepository(String project, String repo) throws IOException { + public Repository retrieveRepository() throws IOException { Request req = new Request.Builder() .get() - .url(format("/rest/api/1.0/projects/%s/repos/%s", project, repo)) + .url(format("%s/rest/api/1.0/projects/%s/repos/%s", config.getUrl(), config.getProject(), config.getRepository())) .build(); try (Response response = okHttpClient.newCall(req).execute()) { validate(response); @@ -206,17 +190,6 @@ public ServerProperties getServerProperties() throws IOException { } } - private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder clientBuilder, BitbucketServerConfiguration config) { - return clientBuilder.addInterceptor(chain -> { - Request newRequest = chain.request().newBuilder() - .addHeader("Authorization", format("Bearer %s", config.getPersonalAccessToken())) - .addHeader("Accept", APPLICATION_JSON_MEDIA_TYPE.toString()) - .build(); - return chain.proceed(newRequest); - }) - .build(); - } - void validate(Response response) throws IOException { if (!response.isSuccessful()) { ErrorResponse errors = null; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactory.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactory.java index 5da3a6c0d..812b94432 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactory.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Marvin Wichmann, Michael Clarke + * Copyright (C) 2020-2022 Marvin Wichmann, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -22,9 +22,10 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.InvalidConfigurationException; -import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.BitbucketCloudConfiguration; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.BitbucketServerConfiguration; import okhttp3.OkHttpClient; +import okhttp3.Request; import okhttp3.logging.HttpLoggingInterceptor; import org.apache.commons.lang3.StringUtils; import org.sonar.api.ce.ComputeEngineSide; @@ -37,7 +38,8 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.util.Optional; -import java.util.function.Supplier; + +import static java.lang.String.format; @ServerSide @ComputeEngineSide @@ -45,15 +47,11 @@ public class DefaultBitbucketClientFactory implements BitbucketClientFactory { private static final Logger LOGGER = Loggers.get(DefaultBitbucketClientFactory.class); - private final Supplier okHttpClientBuilderSupplier; + private final HttpClientBuilderFactory httpClientBuilderFactory; private final Settings settings; - public DefaultBitbucketClientFactory(Settings settings) { - this(settings, OkHttpClient.Builder::new); - } - - DefaultBitbucketClientFactory(Settings settings, Supplier okHttpClientBuilderSupplier) { - this.okHttpClientBuilderSupplier = okHttpClientBuilderSupplier; + public DefaultBitbucketClientFactory(Settings settings, HttpClientBuilderFactory httpClientBuilderFactory) { + this.httpClientBuilderFactory = httpClientBuilderFactory; this.settings = settings; } @@ -61,6 +59,10 @@ public DefaultBitbucketClientFactory(Settings settings) { public BitbucketClient createClient(ProjectAlmSettingDto projectAlmSettingDto, AlmSettingDto almSettingDto) { String almRepo = Optional.ofNullable(StringUtils.trimToNull(projectAlmSettingDto.getAlmRepo())) .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "ALM Repo must be set in configuration")); + + ObjectMapper objectMapper = createObjectMapper(); + OkHttpClient.Builder clientBuilder = createBaseClientBuilder(httpClientBuilderFactory); + if (almSettingDto.getAlm() == ALM.BITBUCKET_CLOUD) { String appId = Optional.ofNullable(StringUtils.trimToNull(almSettingDto.getAppId())) .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "App ID must be set in configuration")); @@ -68,7 +70,8 @@ public BitbucketClient createClient(ProjectAlmSettingDto projectAlmSettingDto, A .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "Client ID must be set in configuration")); String clientSecret = Optional.ofNullable(StringUtils.trimToNull(almSettingDto.getDecryptedClientSecret(settings.getEncryption()))) .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "Client Secret must be set in configuration")); - return new BitbucketCloudClient(new BitbucketCloudConfiguration(appId, almRepo, clientId, clientSecret), createObjectMapper(), createBaseClientBuilder(okHttpClientBuilderSupplier)); + String bearerToken = BitbucketCloudClient.negotiateBearerToken(clientId, clientSecret, objectMapper, clientBuilder.build()); + return new BitbucketCloudClient(objectMapper, createAuthorisingClient(clientBuilder, bearerToken), new BitbucketConfiguration(appId, almRepo)); } else { String almSlug = Optional.ofNullable(StringUtils.trimToNull(projectAlmSettingDto.getAlmSlug())) .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "ALM slug must be set in configuration")); @@ -76,19 +79,30 @@ public BitbucketClient createClient(ProjectAlmSettingDto projectAlmSettingDto, A .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.GLOBAL, "URL must be set in configuration")); String personalAccessToken = Optional.ofNullable(StringUtils.trimToNull(almSettingDto.getDecryptedPersonalAccessToken(settings.getEncryption()))) .orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Personal access token must be set in configuration")); - return new BitbucketServerClient(new BitbucketServerConfiguration(almRepo, almSlug, url, personalAccessToken), createObjectMapper(), createBaseClientBuilder(okHttpClientBuilderSupplier)); + return new BitbucketServerClient(new BitbucketServerConfiguration(almRepo, almSlug, url), objectMapper, createAuthorisingClient(clientBuilder, personalAccessToken)); } } private static ObjectMapper createObjectMapper() { return new ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .findAndRegisterModules(); } - private static OkHttpClient.Builder createBaseClientBuilder(Supplier builderSupplier) { + private static OkHttpClient.Builder createBaseClientBuilder(HttpClientBuilderFactory httpClientBuilderFactory) { HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(LOGGER::debug); httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); - return builderSupplier.get().addInterceptor(httpLoggingInterceptor); + return httpClientBuilderFactory.createClientBuilder().addInterceptor(httpLoggingInterceptor); + } + + private static OkHttpClient createAuthorisingClient(OkHttpClient.Builder clientBuilder, String bearerToken) { + return clientBuilder.addInterceptor(chain -> { + Request newRequest = chain.request().newBuilder() + .addHeader("Authorization", format("Bearer %s", bearerToken)) + .addHeader("Accept", "application/json") + .build(); + return chain.proceed(newRequest); + }).build(); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/HttpClientBuilderFactory.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/HttpClientBuilderFactory.java new file mode 100644 index 000000000..ac2c56192 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/HttpClientBuilderFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket; + +import okhttp3.OkHttpClient; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; + +@ServerSide +@ComputeEngineSide +public class HttpClientBuilderFactory { + + public OkHttpClient.Builder createClientBuilder() { + return new OkHttpClient.Builder(); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/BitbucketConfiguration.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/BitbucketConfiguration.java index 308b6d911..7b15f92df 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/BitbucketConfiguration.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/BitbucketConfiguration.java @@ -23,7 +23,7 @@ public class BitbucketConfiguration { private final String repository; private final String project; - public BitbucketConfiguration(String repository, String project) { + public BitbucketConfiguration(String project, String repository) { this.repository = repository; this.project = project; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/cloud/BitbucketCloudConfiguration.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/ReportStatus.java similarity index 56% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/cloud/BitbucketCloudConfiguration.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/ReportStatus.java index f62f4fc6e..4f2c36955 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/cloud/BitbucketCloudConfiguration.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/ReportStatus.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,26 +16,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ -package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud; +package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model; -import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration; - -public class BitbucketCloudConfiguration extends BitbucketConfiguration { - - private final String clientId; - private final String secret; - - public BitbucketCloudConfiguration(String repository, String project, String clientId, String secret) { - super(repository, project); - this.clientId = clientId; - this.secret = secret; - } - - public String getClientId() { - return clientId; - } - - public String getSecret() { - return secret; - } +public enum ReportStatus { + PASSED, FAILED } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/server/BitbucketServerConfiguration.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/server/BitbucketServerConfiguration.java index 917cddc6b..d3bce53a5 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/server/BitbucketServerConfiguration.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/model/server/BitbucketServerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,19 +23,13 @@ public class BitbucketServerConfiguration extends BitbucketConfiguration { private final String url; - private final String personalAccessToken; - public BitbucketServerConfiguration(String almRepo, String almSlug, String url, String personalAccessToken) { - super(almRepo, almSlug); + public BitbucketServerConfiguration(String project, String repository, String url) { + super(project, repository); this.url = url; - this.personalAccessToken = personalAccessToken; } public String getUrl() { return url; } - - public String getPersonalAccessToken() { - return personalAccessToken; - } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactory.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactory.java index f49015276..99d2c44fa 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactory.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,9 +20,9 @@ import com.github.mc1arke.sonarqube.plugin.InvalidConfigurationException; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.GraphqlGithubClient; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.GraphqlProvider; import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.config.internal.Settings; -import org.sonar.api.platform.Server; import org.sonar.api.server.ServerSide; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; @@ -35,13 +35,13 @@ public class DefaultGithubClientFactory implements GithubClientFactory { private final GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider; - private final Server server; private final Settings settings; + private final GraphqlProvider graphqlProvider; - public DefaultGithubClientFactory(GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider, Server server, Settings settings) { + public DefaultGithubClientFactory(GithubApplicationAuthenticationProvider githubApplicationAuthenticationProvider, Settings settings, GraphqlProvider graphqlProvider) { this.githubApplicationAuthenticationProvider = githubApplicationAuthenticationProvider; - this.server = server; this.settings = settings; + this.graphqlProvider = graphqlProvider; } @Override @@ -55,7 +55,7 @@ public GithubClient createClient(ProjectAlmSettingDto projectAlmSettingDto, AlmS RepositoryAuthenticationToken repositoryAuthenticationToken = githubApplicationAuthenticationProvider.getInstallationToken(apiUrl, appId, apiPrivateKey, projectPath); - return new GraphqlGithubClient(repositoryAuthenticationToken, server); + return new GraphqlGithubClient(graphqlProvider, apiUrl, repositoryAuthenticationToken); } catch (IOException ex) { throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Could not create Github client - " + ex.getMessage(), ex); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/GithubClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/GithubClient.java index 57ca26426..ef6e7c461 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/GithubClient.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/GithubClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,14 +18,12 @@ */ package com.github.mc1arke.sonarqube.plugin.almclient.github; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.CheckRunDetails; import java.io.IOException; public interface GithubClient { - DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto) throws IOException; + String createCheckRun(CheckRunDetails checkRunDetails, boolean postSummaryComment) throws IOException; + + String getRepositoryUrl(); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/RepositoryAuthenticationToken.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/RepositoryAuthenticationToken.java index c05283f11..24aa3073a 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/RepositoryAuthenticationToken.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/RepositoryAuthenticationToken.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,12 +23,16 @@ public class RepositoryAuthenticationToken { private final String repositoryId; private final String authenticationToken; private final String repositoryUrl; + private final String repositoryName; + private final String ownerName; - public RepositoryAuthenticationToken(String repositoryId, String authenticationToken, String repositoryUrl) { + public RepositoryAuthenticationToken(String repositoryId, String authenticationToken, String repositoryUrl, String repositoryName, String ownerName) { super(); this.repositoryId = repositoryId; this.authenticationToken = authenticationToken; this.repositoryUrl = repositoryUrl; + this.repositoryName = repositoryName; + this.ownerName = ownerName; } public String getRepositoryId() { @@ -42,4 +46,12 @@ public String getAuthenticationToken() { public String getRepositoryUrl() { return repositoryUrl; } + + public String getRepositoryName() { + return repositoryName; + } + + public String getOwnerName() { + return ownerName; + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/model/Annotation.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/model/Annotation.java new file mode 100644 index 000000000..51e2c109d --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/model/Annotation.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.almclient.github.model; + +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckAnnotationLevel; + +public class Annotation { + + private final Integer line; + private final String scmPath; + private final CheckAnnotationLevel severity; + private final String message; + + private Annotation(Builder builder) { + line = builder.line; + scmPath = builder.scmPath; + severity = builder.severity; + message = builder.message; + } + + public Integer getLine() { + return line; + } + + public String getScmPath() { + return scmPath; + } + + public CheckAnnotationLevel getSeverity() { + return severity; + } + + public String getMessage() { + return message; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Integer line; + private String scmPath; + private CheckAnnotationLevel severity; + private String message; + + private Builder() { + super(); + } + + public Builder withLine(Integer line) { + this.line = line; + return this; + } + + public Builder withScmPath(String scmPath) { + this.scmPath = scmPath; + return this; + } + + public Builder withSeverity(CheckAnnotationLevel severity) { + this.severity = severity; + return this; + } + + public Builder withMessage(String message) { + this.message = message; + return this; + } + + public Annotation build() { + return new Annotation(this); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/model/CheckRunDetails.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/model/CheckRunDetails.java new file mode 100644 index 000000000..47a4974a6 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/model/CheckRunDetails.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.almclient.github.model; + +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckConclusionState; + +import java.time.ZonedDateTime; +import java.util.List; + +public class CheckRunDetails { + + private final String summary; + private final String title; + private final String name; + private final String dashboardUrl; + private final ZonedDateTime startTime; + private final ZonedDateTime endTime; + private final String externalId; + private final String commitId; + private final List annotations; + private final CheckConclusionState checkConclusionState; + private final int pullRequestId; + + private CheckRunDetails(Builder builder) { + summary = builder.summary; + title = builder.title; + name = builder.name; + dashboardUrl = builder.dashboardUrl; + startTime = builder.startTime; + endTime = builder.endTime; + externalId = builder.externalId; + commitId = builder.commitId; + annotations = builder.annotations; + checkConclusionState = builder.checkConclusionState; + pullRequestId = builder.pullRequestId; + } + + public String getSummary() { + return summary; + } + + public String getTitle() { + return title; + } + + public String getName() { + return name; + } + + public String getDashboardUrl() { + return dashboardUrl; + } + + public ZonedDateTime getStartTime() { + return startTime; + } + + public ZonedDateTime getEndTime() { + return endTime; + } + + public String getExternalId() { + return externalId; + } + + public String getCommitId() { + return commitId; + } + + public List getAnnotations() { + return annotations; + } + + public CheckConclusionState getCheckConclusionState() { + return checkConclusionState; + } + + public int getPullRequestId() { + return pullRequestId; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String summary; + private String title; + private String name; + private String dashboardUrl; + private ZonedDateTime startTime; + private ZonedDateTime endTime; + private String externalId; + private String commitId; + private List annotations; + private CheckConclusionState checkConclusionState; + private int pullRequestId; + + private Builder() { + super(); + } + + public Builder withSummary(String summary) { + this.summary = summary; + return this; + } + + public Builder withTitle(String title) { + this.title = title; + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withDashboardUrl(String dashboardUrl) { + this.dashboardUrl = dashboardUrl; + return this; + } + + public Builder withStartTime(ZonedDateTime startTime) { + this.startTime = startTime; + return this; + } + + public Builder withEndTime(ZonedDateTime endTime) { + this.endTime = endTime; + return this; + } + + public Builder withExternalId(String externalId) { + this.externalId = externalId; + return this; + } + + public Builder withCommitId(String commitId) { + this.commitId = commitId; + return this; + } + + public Builder withAnnotations(List annotations) { + this.annotations = annotations; + return this; + } + + public Builder withCheckConclusionState(CheckConclusionState checkConclusionState) { + this.checkConclusionState = checkConclusionState; + return this; + } + + public Builder withPullRequestId(int pullRequestId) { + this.pullRequestId = pullRequestId; + return this; + } + + public CheckRunDetails build() { + return new CheckRunDetails(this); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/DefaultUrlConnectionProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/DefaultUrlConnectionProvider.java index ca2c98c1c..45ef78896 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/DefaultUrlConnectionProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/DefaultUrlConnectionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,10 +18,15 @@ */ package com.github.mc1arke.sonarqube.plugin.almclient.github.v3; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; + import java.io.IOException; import java.net.URL; import java.net.URLConnection; +@ComputeEngineSide +@ServerSide public final class DefaultUrlConnectionProvider implements UrlConnectionProvider { @Override diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java index f9047cd49..2169a9058 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -64,11 +64,7 @@ public class RestApplicationAuthenticationProvider implements GithubApplicationA private final UrlConnectionProvider urlProvider; private final ObjectMapper objectMapper; - public RestApplicationAuthenticationProvider(LinkHeaderReader linkHeaderReader) { - this(Clock.systemDefaultZone(), linkHeaderReader, new DefaultUrlConnectionProvider()); - } - - RestApplicationAuthenticationProvider(Clock clock, LinkHeaderReader linkHeaderReader, UrlConnectionProvider urlProvider) { + public RestApplicationAuthenticationProvider(Clock clock, LinkHeaderReader linkHeaderReader, UrlConnectionProvider urlProvider) { super(); this.clock = clock; this.urlProvider = urlProvider; @@ -135,7 +131,7 @@ private Optional findRepositoryAuthenticationToke objectMapper.readerFor(InstallationRepositories.class).readValue(installationRepositoriesReader); for (Repository repository : installationRepositories.getRepositories()) { if (projectPath.equals(repository.getFullName())) { - return Optional.of(new RepositoryAuthenticationToken(repository.getNodeId(), appToken.getToken(), repository.getHtmlUrl())); + return Optional.of(new RepositoryAuthenticationToken(repository.getNodeId(), appToken.getToken(), repository.getHtmlUrl(), repository.getName(), repository.getOwner().getLogin())); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Owner.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Owner.java new file mode 100644 index 000000000..1643d966e --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Owner.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Owner { + + private final String login; + + @JsonCreator + public Owner(@JsonProperty("login") String login) { + this.login = login; + } + + public String getLogin() { + return login; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Repository.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Repository.java index c28f2fb1c..025e1d6cb 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Repository.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/model/Repository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -26,12 +26,16 @@ public class Repository { private final String nodeId; private final String fullName; private final String htmlUrl; + private final String name; + private final Owner owner; @JsonCreator - public Repository(@JsonProperty("node_id") String nodeId, @JsonProperty("full_name") String fullName, @JsonProperty("html_url") String htmlUrl) { + public Repository(@JsonProperty("node_id") String nodeId, @JsonProperty("full_name") String fullName, @JsonProperty("html_url") String htmlUrl, @JsonProperty("name") String name, @JsonProperty("owner") Owner owner) { this.nodeId = nodeId; this.fullName = fullName; this.htmlUrl = htmlUrl; + this.name = name; + this.owner = owner; } public String getFullName() { @@ -45,4 +49,12 @@ public String getNodeId() { public String getHtmlUrl() { return htmlUrl; } + + public String getName() { + return name; + } + + public Owner getOwner() { + return owner; + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/DefaultGraphqlProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/DefaultGraphqlProvider.java index c3c1a0f49..191f2cc2e 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/DefaultGraphqlProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/DefaultGraphqlProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,8 +21,12 @@ import io.aexp.nodes.graphql.GraphQLRequestEntity; import io.aexp.nodes.graphql.GraphQLTemplate; import io.aexp.nodes.graphql.InputObject; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; -final class DefaultGraphqlProvider implements GraphqlProvider { +@ComputeEngineSide +@ServerSide +public final class DefaultGraphqlProvider implements GraphqlProvider { @Override public GraphQLTemplate createGraphQLTemplate() { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GetPullRequest.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GetRepository.java similarity index 91% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GetPullRequest.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GetRepository.java index 22e2ed9d1..a8fc0cec1 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GetPullRequest.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GetRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Julien Roy + * Copyright (C) 2021-2022 Julien Roy, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,7 +24,7 @@ import io.aexp.nodes.graphql.annotations.GraphQLProperty; @GraphQLProperty(name = "repository", arguments = {@GraphQLArgument(name = "owner"), @GraphQLArgument(name = "name")}) -public class GetPullRequest { +public class GetRepository { private final String url; @@ -32,7 +32,7 @@ public class GetPullRequest { private final PullRequest pullRequest; @JsonCreator - public GetPullRequest(@JsonProperty("url") String url, @JsonProperty("pullRequest") PullRequest pullRequest) { + public GetRepository(@JsonProperty("url") String url, @JsonProperty("pullRequest") PullRequest pullRequest) { this.url = url; this.pullRequest = pullRequest; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClient.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClient.java index 1eeeeaa4e..771de333b 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClient.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,14 +20,10 @@ import com.github.mc1arke.sonarqube.plugin.almclient.github.GithubClient; import com.github.mc1arke.sonarqube.plugin.almclient.github.RepositoryAuthenticationToken; -import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckAnnotationLevel; -import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckConclusionState; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.Annotation; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.CheckRunDetails; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CommentClassifiers; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.RequestableCheckStatusState; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; import io.aexp.nodes.graphql.Argument; import io.aexp.nodes.graphql.Arguments; import io.aexp.nodes.graphql.GraphQLRequestEntity; @@ -35,22 +31,11 @@ import io.aexp.nodes.graphql.GraphQLTemplate; import io.aexp.nodes.graphql.InputObject; import io.aexp.nodes.graphql.internal.Error; -import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.issue.Issue; -import org.sonar.api.platform.Server; -import org.sonar.api.rule.Severity; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.net.MalformedURLException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.time.Clock; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -58,7 +43,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.TimeZone; import java.util.function.BiFunction; import java.util.stream.Collectors; @@ -67,78 +51,44 @@ public class GraphqlGithubClient implements GithubClient { private static final Logger LOGGER = Loggers.get(GraphqlGithubClient.class); - private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX"; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX") + .withZone(ZoneId.of("UTC")); private static final String INPUT = "input"; private final GraphqlProvider graphqlProvider; - private final Clock clock; private final RepositoryAuthenticationToken repositoryAuthenticationToken; - private final Server server; + private final String apiUrl; - private static final List OPEN_ISSUE_STATUSES = - Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) - .collect(Collectors.toList()); - public GraphqlGithubClient(RepositoryAuthenticationToken repositoryAuthenticationToken, - Server server) { - this(new DefaultGraphqlProvider(), Clock.systemDefaultZone(), repositoryAuthenticationToken, server); - } - - GraphqlGithubClient(GraphqlProvider graphqlProvider, Clock clock, - RepositoryAuthenticationToken repositoryAuthenticationToken, - Server server) { + public GraphqlGithubClient(GraphqlProvider graphqlProvider, String apiUrl, + RepositoryAuthenticationToken repositoryAuthenticationToken) { super(); this.graphqlProvider = graphqlProvider; - this.clock = clock; + this.apiUrl = apiUrl; this.repositoryAuthenticationToken = repositoryAuthenticationToken; - this.server = server; } @Override - public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto) throws IOException { - String apiUrl = Optional.ofNullable(almSettingDto.getUrl()).orElseThrow(() -> new IllegalArgumentException("No URL has been set for Github connections")); - String projectPath = Optional.ofNullable(projectAlmSettingDto.getAlmRepo()).orElseThrow(() -> new IllegalArgumentException("No repository name has been set for Github connections")); - - + public String createCheckRun(CheckRunDetails checkRunDetails, boolean postSummaryComment) throws IOException { Map headers = new HashMap<>(); headers.put("Authorization", "Bearer " + repositoryAuthenticationToken.getAuthenticationToken()); headers.put("Accept", "application/vnd.github.antiope-preview+json"); + List> annotations = createAnnotations(checkRunDetails.getAnnotations()); - String summary = analysisDetails.createAnalysisSummary(new MarkdownFormatterFactory()); - - List issues = analysisDetails.getPostAnalysisIssueVisitor().getIssues(); - - List> annotations = createAnnotations(issues); - - InputObject.Builder checkRunOutputContentBuilder = graphqlProvider.createInputObject().put("title", "Quality Gate " + - (analysisDetails - .getQualityGateStatus() == - QualityGate.Status.OK ? - "success" : - "failed")) - .put("summary", summary) + InputObject.Builder checkRunOutputContentBuilder = graphqlProvider.createInputObject().put("title", checkRunDetails.getTitle()) + .put("summary", checkRunDetails.getSummary()) .put("annotations", annotations); - SimpleDateFormat startedDateFormat = new SimpleDateFormat(DATE_TIME_PATTERN); - startedDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - Map inputObjectArguments = new HashMap<>(); inputObjectArguments.put("repositoryId", repositoryAuthenticationToken.getRepositoryId()); - inputObjectArguments.put("name", String.format("%s Sonarqube Results", analysisDetails.getAnalysisProjectName())); + inputObjectArguments.put("name", checkRunDetails.getName()); inputObjectArguments.put("status", RequestableCheckStatusState.COMPLETED); - inputObjectArguments.put("conclusion", QualityGate.Status.OK == analysisDetails.getQualityGateStatus() ? - CheckConclusionState.SUCCESS : CheckConclusionState.FAILURE); - inputObjectArguments.put("detailsUrl", String.format("%s/dashboard?id=%s&pullRequest=%s", server.getPublicRootUrl(), - URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), - StandardCharsets.UTF_8), URLEncoder - .encode(analysisDetails.getBranchName(), - StandardCharsets.UTF_8))); - inputObjectArguments.put("startedAt", startedDateFormat.format(analysisDetails.getAnalysisDate())); - inputObjectArguments.put("completedAt", DateTimeFormatter.ofPattern(DATE_TIME_PATTERN).withZone(ZoneId.of("UTC")) - .format(clock.instant())); - inputObjectArguments.put("externalId", analysisDetails.getAnalysisId()); + inputObjectArguments.put("conclusion", checkRunDetails.getCheckConclusionState()); + inputObjectArguments.put("detailsUrl", checkRunDetails.getDashboardUrl()); + inputObjectArguments.put("startedAt", DATE_TIME_FORMATTER.format(checkRunDetails.getStartTime())); + inputObjectArguments.put("completedAt", DATE_TIME_FORMATTER.format(checkRunDetails.getEndTime())); + inputObjectArguments.put("externalId", checkRunDetails.getExternalId()); inputObjectArguments.put("output", checkRunOutputContentBuilder.build()); InputObject.Builder repositoryInputObjectBuilder = graphqlProvider.createInputObject(); @@ -152,7 +102,7 @@ public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSetti .headers(headers) .request(CreateCheckRun.class) .arguments(new Arguments("createCheckRun", new Argument<>(INPUT, repositoryInputObjectBuilder - .put("headSha", analysisDetails.getCommitSha()) + .put("headSha", checkRunDetails.getCommitId()) .build()))) .requestMethod(GraphQLTemplate.GraphQLMethod.MUTATE); @@ -161,31 +111,30 @@ public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSetti GraphQLResponseEntity graphQLResponseEntity = executeRequest((r, t) -> graphqlProvider.createGraphQLTemplate().mutate(r, t), graphQLRequestEntity, CreateCheckRun.class); - reportRemainingIssues(issues, graphQLResponseEntity.getResponse().getCheckRun().getId(), + reportRemainingAnnotations(checkRunDetails.getAnnotations(), graphQLResponseEntity.getResponse().getCheckRun().getId(), inputObjectArguments, checkRunOutputContentBuilder, graphQLRequestEntityBuilder); - if (Optional.ofNullable(projectAlmSettingDto.getSummaryCommentEnabled()).orElse(true)) { - postSummaryComment(graphqlUrl, headers, projectPath, analysisDetails.getBranchName(), summary); + if (postSummaryComment) { + postSummaryComment(graphqlUrl, headers, checkRunDetails.getPullRequestId(), checkRunDetails.getSummary()); } - return DecorationResult.builder() - .withPullRequestUrl(repositoryAuthenticationToken.getRepositoryUrl() + "/pull/" + analysisDetails.getBranchName()) - .build(); + return graphQLResponseEntity.getResponse().getCheckRun().getId(); } - private void postSummaryComment(String graphqlUrl, Map headers, String projectPath, String pullRequestKey, String summary) throws IOException { - String login = getLogin(graphqlUrl, headers); + @Override + public String getRepositoryUrl() { + return repositoryAuthenticationToken.getRepositoryUrl(); + } - String[] paths = projectPath.split("/", 2); - String owner = paths[0]; - String projectName = paths[1]; + private void postSummaryComment(String graphqlUrl, Map headers, int pullRequestKey, String summary) throws IOException { + String login = getLogin(graphqlUrl, headers); - GetPullRequest.PullRequest pullRequest = getPullRequest(graphqlUrl, headers, projectName, pullRequestKey, owner); + GetRepository.PullRequest pullRequest = getPullRequest(graphqlUrl, headers, pullRequestKey); String pullRequestId = pullRequest.getId(); - getComments(pullRequest, graphqlUrl, headers, projectName, pullRequestKey, owner).stream() + getComments(pullRequest, graphqlUrl, headers, pullRequestKey).stream() .filter(c -> "Bot".equalsIgnoreCase(c.getAuthor().getType()) && login.equalsIgnoreCase(c.getAuthor().getLogin())) .filter(c -> !c.isMinimized()) .map(Comments.CommentNode::getId) @@ -211,36 +160,36 @@ private void postSummaryComment(String graphqlUrl, Map headers, } - private List getComments(GetPullRequest.PullRequest pullRequest, String graphqlUrl, Map headers, String projectName, String pullRequestKey, String owner) throws MalformedURLException { + private List getComments(GetRepository.PullRequest pullRequest, String graphqlUrl, Map headers, int pullRequestKey) throws MalformedURLException { List comments = new ArrayList<>(pullRequest.getComments().getNodes()); PageInfo currentPageInfo = pullRequest.getComments().getPageInfo(); if (currentPageInfo.hasNextPage()) { - GetPullRequest.PullRequest response = getPullRequest(graphqlUrl, headers, projectName, pullRequestKey, owner, currentPageInfo); - comments.addAll(getComments(response, graphqlUrl, headers, projectName, pullRequestKey, owner)); + GetRepository.PullRequest response = getPullRequest(graphqlUrl, headers, pullRequestKey, currentPageInfo); + comments.addAll(getComments(response, graphqlUrl, headers, pullRequestKey)); } return comments; } - private GetPullRequest.PullRequest getPullRequest(String graphqlUrl, Map headers, String projectName, String pullRequestKey, String owner) throws MalformedURLException { - return getPullRequest(graphqlUrl, headers, projectName, pullRequestKey, owner, null); + private GetRepository.PullRequest getPullRequest(String graphqlUrl, Map headers, int pullRequestKey) throws MalformedURLException { + return getPullRequest(graphqlUrl, headers, pullRequestKey, null); } - private GetPullRequest.PullRequest getPullRequest(String graphqlUrl, Map headers, String projectName, String pullRequestKey, String owner, PageInfo pageInfo) throws MalformedURLException { + private GetRepository.PullRequest getPullRequest(String graphqlUrl, Map headers, int pullRequestKey, PageInfo pageInfo) throws MalformedURLException { GraphQLRequestEntity getPullRequest = graphqlProvider.createRequestBuilder() .url(graphqlUrl) .headers(headers) - .request(GetPullRequest.class) + .request(GetRepository.class) .arguments( - new Arguments("repository", new Argument<>("owner", owner), new Argument<>("name", projectName)), - new Arguments("repository.pullRequest", new Argument<>("number", Integer.valueOf(pullRequestKey))), + new Arguments("repository", new Argument<>("owner", repositoryAuthenticationToken.getOwnerName()), new Argument<>("name", repositoryAuthenticationToken.getRepositoryName())), + new Arguments("repository.pullRequest", new Argument<>("number", pullRequestKey)), new Arguments("repository.pullRequest.comments", new Argument<>("first", 100), new Argument<>("after", Optional.ofNullable(pageInfo).map(PageInfo::getEndCursor).orElse(null))) ) .build(); - return executeRequest((r, t) -> graphqlProvider.createGraphQLTemplate().query(r, t), getPullRequest, GetPullRequest.class).getResponse().getPullRequest(); + return executeRequest((r, t) -> graphqlProvider.createGraphQLTemplate().query(r, t), getPullRequest, GetRepository.class).getResponse().getPullRequest(); } private void minimizeComment(String graphqlUrl, Map headers, String commentId) { @@ -300,18 +249,18 @@ private static GraphQLResponseEntity executeRequest( return response; } - private void reportRemainingIssues(List outstandingIssues, String checkRunId, - Map repositoryInputArguments, InputObject.Builder outputObjectBuilder, - GraphQLRequestEntity.RequestBuilder graphQLRequestEntityBuilder) { + private void reportRemainingAnnotations(List outstandingAnnotations, String checkRunId, + Map repositoryInputArguments, InputObject.Builder outputObjectBuilder, + GraphQLRequestEntity.RequestBuilder graphQLRequestEntityBuilder) { - if (outstandingIssues.size() <= 50) { + if (outstandingAnnotations.size() <= 50) { return; } - List issues = outstandingIssues.subList(50, outstandingIssues.size()); + List annotations = outstandingAnnotations.subList(50, outstandingAnnotations.size()); InputObject outputObject = outputObjectBuilder - .put("annotations", createAnnotations(issues)) + .put("annotations", createAnnotations(annotations)) .build(); InputObject.Builder repositoryInputObjectBuilder = graphqlProvider.createInputObject(); @@ -330,25 +279,23 @@ private void reportRemainingIssues(List executeRequest((r, t) -> graphqlProvider.createGraphQLTemplate().mutate(r, t), graphQLRequestEntity, UpdateCheckRun.class); - reportRemainingIssues(issues, checkRunId, repositoryInputArguments, outputObjectBuilder, graphQLRequestEntityBuilder); + reportRemainingAnnotations(annotations, checkRunId, repositoryInputArguments, outputObjectBuilder, graphQLRequestEntityBuilder); } - private List> createAnnotations(List issues) { - return issues.stream() + private List> createAnnotations(List annotations) { + return annotations.stream() .limit(50) - .filter(i -> i.getComponent().getReportAttributes().getScmPath().isPresent()) - .filter(i -> i.getComponent().getType() == Component.Type.FILE) - .filter(i -> i.getIssue().resolution() == null) - .filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status())).map(componentIssue -> { + .map(annotation -> { InputObject issueLocation = graphqlProvider.createInputObject() - .put("startLine", Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0)) - .put("endLine", Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0)) + .put("startLine", Optional.ofNullable(annotation.getLine()).orElse(0)) + .put("endLine", Optional.ofNullable(annotation.getLine()).orElse(0)) .build(); return graphqlProvider.createInputObject() - .put("path", componentIssue.getComponent().getReportAttributes().getScmPath().get()) + .put("path", annotation.getScmPath()) .put("location", issueLocation) - .put("annotationLevel", mapToGithubAnnotationLevel(componentIssue.getIssue().severity())) - .put("message", componentIssue.getIssue().getMessage().replace("\\","\\\\").replace("\"", "\\\"")).build(); + .put("annotationLevel", annotation.getSeverity()) + .put("message", annotation.getMessage()) + .build(); }).collect(Collectors.toList()); } @@ -364,19 +311,4 @@ private static String getGraphqlUrl(String apiUrl) { return apiUrl; } - private static CheckAnnotationLevel mapToGithubAnnotationLevel(String sonarqubeSeverity) { - switch (sonarqubeSeverity) { - case Severity.INFO: - return CheckAnnotationLevel.NOTICE; - case Severity.MINOR: - case Severity.MAJOR: - return CheckAnnotationLevel.WARNING; - case Severity.CRITICAL: - case Severity.BLOCKER: - return CheckAnnotationLevel.FAILURE; - default: - throw new IllegalArgumentException("Unknown severity value: " + sonarqubeSeverity); - } - } - } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlProvider.java index 2c21a4c46..bdca4b172 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -22,7 +22,7 @@ import io.aexp.nodes.graphql.GraphQLTemplate; import io.aexp.nodes.graphql.InputObject; -interface GraphqlProvider { +public interface GraphqlProvider { GraphQLTemplate createGraphQLTemplate(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/model/MergeRequest.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/model/MergeRequest.java index 30c7d3d89..c37f53c64 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/model/MergeRequest.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/model/MergeRequest.java @@ -24,15 +24,18 @@ public class MergeRequest { private final long iid; private final DiffRefs diffRefs; private final long sourceProjectId; + private final long targetProjectId; private final String webUrl; public MergeRequest(@JsonProperty("iid") long iid, @JsonProperty("diff_refs") DiffRefs diffRefs, @JsonProperty("source_project_id") long sourceProjectId, + @JsonProperty("target_project_id") long targetProjectId, @JsonProperty("web_url") String webUrl) { this.iid = iid; this.diffRefs = diffRefs; this.sourceProjectId = sourceProjectId; this.webUrl = webUrl; + this.targetProjectId = targetProjectId; } public long getIid() { @@ -47,6 +50,10 @@ public long getSourceProjectId() { return sourceProjectId; } + public long getTargetProjectId() { + return targetProjectId; + } + public String getWebUrl() { return webUrl; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java index 9b0b700bf..e6158a13c 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,8 +21,11 @@ import com.github.mc1arke.sonarqube.plugin.almclient.DefaultLinkHeaderReader; import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.DefaultBitbucketClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.HttpClientBuilderFactory; import com.github.mc1arke.sonarqube.plugin.almclient.github.DefaultGithubClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.DefaultUrlConnectionProvider; import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.DefaultGraphqlProvider; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestPostAnalysisTask; @@ -30,6 +33,8 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.BitbucketPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.GitlabMergeRequestDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import org.sonar.ce.task.projectanalysis.container.ReportAnalysisComponentProvider; import java.util.Arrays; @@ -43,9 +48,10 @@ public class CommunityReportAnalysisComponentProvider implements ReportAnalysisC @Override public List getComponents() { return Arrays.asList(CommunityBranchLoaderDelegate.class, PullRequestPostAnalysisTask.class, - PostAnalysisIssueVisitor.class, DefaultLinkHeaderReader.class, + PostAnalysisIssueVisitor.class, DefaultLinkHeaderReader.class, ReportGenerator.class, + MarkdownFormatterFactory.class, DefaultGraphqlProvider.class, DefaultUrlConnectionProvider.class, DefaultGithubClientFactory.class, RestApplicationAuthenticationProvider.class, GithubPullRequestDecorator.class, - DefaultBitbucketClientFactory.class, BitbucketPullRequestDecorator.class, + HttpClientBuilderFactory.class, DefaultBitbucketClientFactory.class, BitbucketPullRequestDecorator.class, DefaultGitlabClientFactory.class, GitlabMergeRequestDecorator.class, DefaultAzureDevopsClientFactory.class, AzureDevOpsPullRequestDecorator.class); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java index a4dece7cb..4011b277c 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetails.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,469 +19,101 @@ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; -import com.github.mc1arke.sonarqube.plugin.CommunityBranchPlugin; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Heading; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Image; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Link; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.ListItem; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Node; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; -import org.apache.commons.lang.StringUtils; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; import org.sonar.api.ce.posttask.Analysis; +import org.sonar.api.ce.posttask.PostProjectAnalysisTask; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.ce.posttask.QualityGate.EvaluationStatus; -import org.sonar.api.ce.posttask.ScannerContext; -import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Metric; -import org.sonar.api.rules.RuleType; import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; -import org.sonar.ce.task.projectanalysis.measure.Measure; -import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; -import org.sonar.ce.task.projectanalysis.metric.MetricRepository; -import org.sonar.server.measure.Rating; -import java.io.UnsupportedEncodingException; -import java.math.BigDecimal; -import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.text.NumberFormat; -import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class AnalysisDetails { - private static final List CLOSED_ISSUE_STATUS = Arrays.asList(Issue.STATUS_CLOSED, Issue.STATUS_RESOLVED); + private static final List OPEN_ISSUE_STATUSES = + Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) + .collect(Collectors.toList()); - private static final List COVERAGE_LEVELS = - Arrays.asList(BigDecimal.valueOf(100), BigDecimal.valueOf(90), BigDecimal.valueOf(60), - BigDecimal.valueOf(50), BigDecimal.valueOf(40), BigDecimal.valueOf(25)); - private static final List DUPLICATION_LEVELS = - Arrays.asList(new DuplicationMapping(BigDecimal.valueOf(3), "3"), - new DuplicationMapping(BigDecimal.valueOf(5), "5"), - new DuplicationMapping(BigDecimal.TEN, "10"), - new DuplicationMapping(BigDecimal.valueOf(20), "20")); - - private final String publicRootURL; - private final BranchDetails branchDetails; - private final MeasuresHolder measuresHolder; - private final PostAnalysisIssueVisitor postAnalysisIssueVisitor; + private final String pullRequestId; + private final String commitId; + private final List issues; private final QualityGate qualityGate; - private final Analysis analysis; - private final Project project; - private final ScannerContext scannerContext; - private final Configuration configuration; + private final PostProjectAnalysisTask.ProjectAnalysis projectAnalysis; - AnalysisDetails(BranchDetails branchDetails, PostAnalysisIssueVisitor postAnalysisIssueVisitor, - QualityGate qualityGate, MeasuresHolder measuresHolder, Analysis analysis, Project project, - Configuration configuration, String publicRootURL, ScannerContext scannerContext) { + AnalysisDetails(String pullRequestId, String commitId, List issues, + QualityGate qualityGate, PostProjectAnalysisTask.ProjectAnalysis projectAnalysis) { super(); - this.publicRootURL = publicRootURL; - this.branchDetails = branchDetails; - this.measuresHolder = measuresHolder; - this.postAnalysisIssueVisitor = postAnalysisIssueVisitor; + this.pullRequestId = pullRequestId; + this.commitId = commitId; + this.issues = issues; this.qualityGate = qualityGate; - this.analysis = analysis; - this.project = project; - this.scannerContext = scannerContext; - this.configuration = configuration; + this.projectAnalysis = projectAnalysis; } - public String getBranchName() { - return branchDetails.getBranchName(); + public String getPullRequestId() { + return pullRequestId; } public String getCommitSha() { - return branchDetails.getCommitId(); - } - - public String getDashboardUrl() { - return publicRootURL + "/dashboard?id=" + encode(project.getKey()) + "&pullRequest=" + branchDetails.getBranchName(); - } - - public String getIssueUrl(PostAnalysisIssueVisitor.LightIssue issue) { - if (issue.type() == RuleType.SECURITY_HOTSPOT) { - return String.format("%s/security_hotspots?id=%s&pullRequest=%s&hotspots=%s", publicRootURL, encode(project.getKey()), branchDetails.getBranchName(), issue.key()); - } else { - return String.format("%s/project/issues?id=%s&pullRequest=%s&issues=%s&open=%s", publicRootURL, encode(project.getKey()), branchDetails.getBranchName(), issue.key(), issue.key()); - } - } - - public Optional parseIssueIdFromUrl(String issueUrl) { - URI url = URI.create(issueUrl); - List parameters = URLEncodedUtils.parse(url, StandardCharsets.UTF_8); - Optional optionalProjectId = parameters.stream() - .filter(parameter -> "id".equals(parameter.getName())) - .map(NameValuePair::getValue) - .findFirst(); - - if (optionalProjectId.isEmpty()) { - return Optional.empty(); - } - - String projectId = optionalProjectId.get(); - - if (url.getPath().endsWith("/dashboard")) { - return Optional.of(new ProjectIssueIdentifier(projectId, "decorator-summary-comment")); - } else if (url.getPath().endsWith("security_hotspots")) { - return parameters.stream() - .filter(parameter -> "hotspots".equals(parameter.getName())) - .map(NameValuePair::getValue) - .findFirst() - .map(issueId -> new ProjectIssueIdentifier(projectId, issueId)); - } else { - return parameters.stream() - .filter(parameter -> "issues".equals(parameter.getName())) - .map(NameValuePair::getValue) - .findFirst() - .map(issueId -> new ProjectIssueIdentifier(projectId, issueId)); - } + return commitId; } public QualityGate.Status getQualityGateStatus() { return qualityGate.getStatus(); } - public String getRuleUrlWithRuleKey(String ruleKey) { - return publicRootURL + "/coding_rules?open=" + encode(ruleKey) + "&rule_key=" + encode(ruleKey); + public List findFailedQualityGateConditions() { + return qualityGate.getConditions().stream() + .filter(c -> c.getStatus() == QualityGate.EvaluationStatus.ERROR) + .collect(Collectors.toList()); } public Optional getScannerProperty(String propertyName) { - return Optional.ofNullable(scannerContext.getProperties().get(propertyName)); + return Optional.ofNullable(projectAnalysis.getScannerContext().getProperties().get(propertyName)); } - public String createAnalysisSummary(FormatterFactory formatterFactory) { - - BigDecimal newCoverage = getNewCoverage().orElse(null); - BigDecimal coverage = getCoverage().orElse(null); - - BigDecimal newDuplications = findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY) - .filter(condition -> condition.getStatus() != EvaluationStatus.NO_VALUE) - .map(QualityGate.Condition::getValue) - .map(BigDecimal::new) - .orElse(null); - - double duplications = - findMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY_KEY).map(Measure::getDoubleValue).orElse(0D); - - NumberFormat decimalFormat = new DecimalFormat("#0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); - - Map issueCounts = countRuleByType(); - long issueTotal = issueCounts.values().stream().mapToLong(l -> l).sum(); - - List failedConditions = findFailedConditions(); - - String baseImageUrl = getBaseImageUrl(); - - Document document = new Document(new Paragraph((QualityGate.Status.OK == getQualityGateStatus() ? - new Image("Passed", baseImageUrl + - "/checks/QualityGateBadge/passed.svg?sanitize=true") : - new Image("Failed", baseImageUrl + - "/checks/QualityGateBadge/failed.svg?sanitize=true"))), - failedConditions.isEmpty() ? new Text("") : - new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( - com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, - failedConditions.stream().map(c -> new ListItem(new Text(format(c)))) - .toArray(ListItem[]::new)), - new Heading(1, new Text("Analysis Details")), new Heading(2, new Text( - issueTotal + " Issue" + (issueCounts.values().stream().mapToLong(l -> l).sum() == 1 ? "" : "s"))), - new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( - com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, - new ListItem(new Image("Bug", - baseImageUrl + "/common/bug.svg?sanitize=true"), - new Text(" "), new Text( - pluralOf(issueCounts.get(RuleType.BUG), "Bug", "Bugs"))), - new ListItem(new Image("Vulnerability", baseImageUrl + - "/common/vulnerability.svg?sanitize=true"), - new Text(" "), new Text(pluralOf( - issueCounts.get(RuleType.VULNERABILITY) + - issueCounts.get(RuleType.SECURITY_HOTSPOT), "Vulnerability", - "Vulnerabilities"))), new ListItem(new Image("Code Smell", - baseImageUrl + - "/common/code_smell.svg?sanitize=true"), - new Text(" "), new Text( - pluralOf(issueCounts.get(RuleType.CODE_SMELL), "Code Smell", - "Code Smells")))), - new Heading(2, new Text("Coverage and Duplications")), - new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( - com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, - new ListItem(createCoverageImage(newCoverage, baseImageUrl), - new Text(" "), new Text( - Optional.ofNullable(newCoverage).map(decimalFormat::format) - .map(i -> i + "% Coverage") - .orElse("No coverage information") + " (" + - decimalFormat.format(Optional.ofNullable(coverage).orElse(BigDecimal.valueOf(0))) + "% Estimated after merge)")), - new ListItem(createDuplicateImage(newDuplications, baseImageUrl), - new Text(" "), new Text( - Optional.ofNullable(newDuplications).map(decimalFormat::format) - .map(i -> i + "% Duplicated Code") - .orElse("No duplication information") + " (" + - decimalFormat.format(duplications) + - "% Estimated after merge)"))), - new Paragraph(new Text(String.format("**Project ID:** %s", project.getKey()))), - new Paragraph(new Link(getDashboardUrl(), new Text("View in SonarQube")))); - - return formatterFactory.documentFormatter().format(document, formatterFactory); - } - - public String createAnalysisIssueSummary(PostAnalysisIssueVisitor.ComponentIssue componentIssue, FormatterFactory formatterFactory) { - final PostAnalysisIssueVisitor.LightIssue issue = componentIssue.getIssue(); - - String baseImageUrl = getBaseImageUrl(); - - Long effort = issue.effortInMinutes(); - Node effortNode = (null == effort ? new Text("") : new Paragraph(new Text(String.format("**Duration (min):** %s", effort)))); - - String resolution = issue.resolution(); - Node resolutionNode = (StringUtils.isBlank(resolution) ? new Text("") : new Paragraph(new Text(String.format("**Resolution:** %s ", resolution)))); - - Document document = new Document( - new Paragraph(new Text(String.format("**Type:** %s ", issue.type().name())), new Image(issue.type().name(), String.format("%s/checks/IssueType/%s.svg?sanitize=true", baseImageUrl, issue.type().name().toLowerCase()))), - new Paragraph(new Text(String.format("**Severity:** %s ", issue.severity())), new Image(issue.severity(), String.format("%s/checks/Severity/%s.svg?sanitize=true", baseImageUrl, issue.severity().toLowerCase()))), - new Paragraph(new Text(String.format("**Message:** %s", issue.getMessage()))), - effortNode, - resolutionNode, - new Paragraph(new Text(String.format("**Project ID:** %s **Issue ID:** %s", project.getKey(), issue.key()))), - new Paragraph(new Link(getIssueUrl(issue), new Text("View in SonarQube"))) - ); - return formatterFactory.documentFormatter().format(document, formatterFactory); - } - - public String getBaseImageUrl() { - return configuration.get(CommunityBranchPlugin.IMAGE_URL_BASE) - .orElse(publicRootURL + "/static/communityBranchPlugin") - .replaceAll("/*$", ""); - } - - public Optional getSCMPathForIssue(PostAnalysisIssueVisitor.ComponentIssue componentIssue) { - Component component = componentIssue.getComponent(); - if (Component.Type.FILE.equals(component.getType())) { - return component.getReportAttributes().getScmPath(); - } - return Optional.empty(); - } - - public PostAnalysisIssueVisitor getPostAnalysisIssueVisitor() { - return postAnalysisIssueVisitor; - } - - private static String encode(String original) { - try { - return URLEncoder.encode(original, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("Standard charset not found in JVM", e); - } - } - - private static Image createCoverageImage(BigDecimal coverage, String baseImageUrl) { - if (null == coverage) { - return new Image("No coverage information", - baseImageUrl + "/checks/CoverageChart/NoCoverageInfo.svg?sanitize=true"); - } - BigDecimal matchedLevel = BigDecimal.ZERO; - for (BigDecimal level : COVERAGE_LEVELS) { - if (coverage.compareTo(level) >= 0) { - matchedLevel = level; - break; - } - } - return new Image(matchedLevel + " percent coverage", - baseImageUrl + "/checks/CoverageChart/" + matchedLevel + ".svg?sanitize=true"); - } - - private static Image createDuplicateImage(BigDecimal duplications, String baseImageUrl) { - if (null == duplications) { - return new Image("No duplication information", - baseImageUrl + "/checks/Duplications/NoDuplicationInfo.svg?sanitize=true"); - } - String matchedLevel = "20plus"; - for (DuplicationMapping level : DUPLICATION_LEVELS) { - if (level.getDuplicationLevel().compareTo(duplications) >= 0) { - matchedLevel = level.getImageName(); - break; - } - } - return new Image(matchedLevel + " percent duplication", - baseImageUrl + "/checks/Duplications/" + matchedLevel + ".svg?sanitize=true"); - } - - public Date getAnalysisDate() { - return analysis.getDate(); + return getAnalysis().getDate(); } public String getAnalysisId() { - return analysis.getAnalysisUuid(); + return getAnalysis().getAnalysisUuid(); } public String getAnalysisProjectKey() { - return project.getKey(); + return getProject().getKey(); } public String getAnalysisProjectName() { - return project.getName(); + return getProject().getName(); } - public List findFailedConditions() { - return qualityGate.getConditions().stream().filter(c -> c.getStatus() == QualityGate.EvaluationStatus.ERROR) - .collect(Collectors.toList()); + public List getIssues() { + return issues; } - public Optional findMeasure(String metricKey) { - return measuresHolder.getMeasureRepository().getRawMeasure(measuresHolder.getTreeRootHolder().getRoot(), - measuresHolder.getMetricRepository() - .getByKey(metricKey)); + public List getScmReportableIssues() { + return getIssues().stream() + .filter(i -> i.getComponent().getReportAttributes().getScmPath().isPresent()) + .filter(i -> i.getComponent().getType() == Component.Type.FILE) + .filter(i -> i.getIssue().resolution() == null) + .filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status())) + .collect(Collectors.toList()); } public Optional findQualityGateCondition(String metricKey) { return qualityGate.getConditions().stream().filter(c -> metricKey.equals(c.getMetricKey())).findFirst(); } - public Map countRuleByType() { - return Arrays.stream(RuleType.values()).collect(Collectors.toMap(k -> k, - k -> postAnalysisIssueVisitor.getIssues() - .stream() - .map(PostAnalysisIssueVisitor.ComponentIssue::getIssue) - .filter(i -> !CLOSED_ISSUE_STATUS - .contains(i.status())) - .filter(i -> k == i.type()).count())); - } - - private static String pluralOf(long value, String singleLabel, String multiLabel) { - return value + " " + (1 == value ? singleLabel : multiLabel); - } - - - public static String format(QualityGate.Condition condition) { - Metric metric = CoreMetrics.getMetric(condition.getMetricKey()); - if (metric.getType() == Metric.ValueType.RATING) { - return String - .format("%s %s (%s %s)", Rating.valueOf(Integer.parseInt(condition.getValue())), metric.getName(), - condition.getOperator() == QualityGate.Operator.GREATER_THAN ? "is worse than" : - "is better than", Rating.valueOf(Integer.parseInt(condition.getErrorThreshold()))); - } else if (metric.getType() == Metric.ValueType.PERCENT) { - NumberFormat numberFormat = new DecimalFormat("#0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); - return String.format("%s%% %s (%s %s%%)", numberFormat.format(new BigDecimal(condition.getValue())), - metric.getName(), - condition.getOperator() == QualityGate.Operator.GREATER_THAN ? "is greater than" : - "is less than", numberFormat.format(new BigDecimal(condition.getErrorThreshold()))); - } else { - return String.format("%s %s (%s %s)", condition.getValue(), metric.getName(), - condition.getOperator() == QualityGate.Operator.GREATER_THAN ? "is greater than" : - "is less than", condition.getErrorThreshold()); - } - } - - public Optional getNewCoverage(){ - return findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) - .filter(condition -> condition.getStatus() != EvaluationStatus.NO_VALUE) - .map(QualityGate.Condition::getValue) - .map(BigDecimal::new); - } - - public Optional getCoverage(){ - return findMeasure(CoreMetrics.COVERAGE_KEY).map(Measure::getDoubleValue).map(BigDecimal::new); - } - - public static class BranchDetails { - - private final String branchName; - private final String commitId; - - BranchDetails(String branchName, String commitId) { - this.branchName = branchName; - this.commitId = commitId; - } - - public String getBranchName() { - return branchName; - } - - public String getCommitId() { - return commitId; - } - - } - - public static class MeasuresHolder { - - private final MetricRepository metricRepository; - private final MeasureRepository measureRepository; - private final TreeRootHolder treeRootHolder; - - MeasuresHolder(MetricRepository metricRepository, MeasureRepository measureRepository, - TreeRootHolder treeRootHolder) { - this.metricRepository = metricRepository; - this.measureRepository = measureRepository; - this.treeRootHolder = treeRootHolder; - } - - public MetricRepository getMetricRepository() { - return metricRepository; - } - - public MeasureRepository getMeasureRepository() { - return measureRepository; - } - - public TreeRootHolder getTreeRootHolder() { - return treeRootHolder; - } - + private Analysis getAnalysis() { + return projectAnalysis.getAnalysis().orElseThrow(); } - private static class DuplicationMapping { - - private final BigDecimal duplicationLevel; - private final String imageName; - - DuplicationMapping(BigDecimal duplicationLevel, String imageName) { - this.duplicationLevel = duplicationLevel; - this.imageName = imageName; - } - - private BigDecimal getDuplicationLevel() { - return duplicationLevel; - } - - private String getImageName() { - return imageName; - } - } - - public static class ProjectIssueIdentifier { - - private final String projectKey; - private final String issueKey; - - public ProjectIssueIdentifier(String projectKey, String issueKey) { - this.projectKey = projectKey; - this.issueKey = issueKey; - } - - public String getProjectKey() { - return projectKey; - } - - public String getIssueKey() { - return issueKey; - } + private Project getProject() { + return projectAnalysis.getProject(); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/DiscussionAwarePullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/DiscussionAwarePullRequestDecorator.java index 6d5b8f330..627f11fc2 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/DiscussionAwarePullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/DiscussionAwarePullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,12 +18,15 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutableTriple; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; -import org.sonar.api.issue.Issue; -import org.sonar.api.platform.Server; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; import org.sonar.db.alm.setting.AlmSettingDto; @@ -32,6 +35,8 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -45,20 +50,16 @@ public abstract class DiscussionAwarePullRequestDecorator impleme "This issue no longer exists in SonarQube, but due to other comments being present in this discussion, the discussion is not being being closed automatically. " + "Please manually resolve this discussion once the other comments have been reviewed."; - private static final List OPEN_ISSUE_STATUSES = - Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) - .collect(Collectors.toList()); - private static final String VIEW_IN_SONARQUBE_LABEL = "View in SonarQube"; private static final Pattern NOTE_MARKDOWN_VIEW_LINK_PATTERN = Pattern.compile("^\\[" + VIEW_IN_SONARQUBE_LABEL + "]\\((.*?)\\)$"); - private final Server server; private final ScmInfoRepository scmInfoRepository; + private final ReportGenerator reportGenerator; - protected DiscussionAwarePullRequestDecorator(Server server, ScmInfoRepository scmInfoRepository) { + protected DiscussionAwarePullRequestDecorator(ScmInfoRepository scmInfoRepository, ReportGenerator reportGenerator) { super(); - this.server = server; this.scmInfoRepository = scmInfoRepository; + this.reportGenerator = reportGenerator; } @Override @@ -68,14 +69,11 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, AlmS P pullRequest = getPullRequest(client, almSettingDto, projectAlmSettingDto, analysis); U user = getCurrentUser(client); - List openSonarqubeIssues = analysis.getPostAnalysisIssueVisitor().getIssues().stream() - .filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())) - .collect(Collectors.toList()); + List openSonarqubeIssues = analysis.getScmReportableIssues(); - List>> currentProjectSonarqueComments = findOpenSonarqubeComments(client, + List>> currentProjectSonarqueComments = findOpenSonarqubeComments(client, pullRequest, - user, - analysis) + user) .stream() .filter(comment -> isCommentFromCurrentProject(comment, analysis.getAnalysisProjectKey())) .collect(Collectors.toList()); @@ -90,7 +88,7 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, AlmS List> uncommentedIssues = findIssuesWithoutComments(openSonarqubeIssues, commentKeysForOpenComments) .stream() - .map(issue -> loadScmPathsForIssues(issue, analysis)) + .map(DiscussionAwarePullRequestDecorator::loadScmPathsForIssues) .filter(Optional::isPresent) .map(Optional::get) .filter(issue -> isIssueFromCommitInCurrentRequest(issue.getLeft(), commitIds, scmInfoRepository)) @@ -100,9 +98,12 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, AlmS pullRequest, issue.getLeft(), issue.getRight(), - analysis)); - submitSummaryNote(client, pullRequest, analysis); - submitPipelineStatus(client, pullRequest, analysis, server.getPublicRootUrl()); + analysis, + reportGenerator.createAnalysisIssueSummary(issue.getLeft(), analysis))); + + AnalysisSummary analysisSummary = reportGenerator.createAnalysisSummary(analysis); + submitSummaryNote(client, pullRequest, analysis, analysisSummary); + submitPipelineStatus(client, pullRequest, analysis, analysisSummary); DecorationResult.Builder builder = DecorationResult.builder(); createFrontEndUrl(pullRequest, analysis).ifPresent(builder::withPullRequestUrl); @@ -119,10 +120,10 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, AlmS protected abstract List getCommitIdsForPullRequest(C client, P pullRequest); - protected abstract void submitPipelineStatus(C client, P pullRequest, AnalysisDetails analysis, String sonarqubeRootUrl); + protected abstract void submitPipelineStatus(C client, P pullRequest, AnalysisDetails analysis, AnalysisSummary analysisSummary); protected abstract void submitCommitNoteForIssue(C client, P pullRequest, PostAnalysisIssueVisitor.ComponentIssue issue, String filePath, - AnalysisDetails analysis); + AnalysisDetails analysis, AnalysisIssueSummary analysisIssueSummary); protected abstract String getNoteContent(C client, N note); @@ -136,7 +137,7 @@ protected abstract void submitCommitNoteForIssue(C client, P pullRequest, PostAn protected abstract void resolveDiscussion(C client, D discussion, P pullRequest); - protected abstract void submitSummaryNote(C client, P pullRequest, AnalysisDetails analysis); + protected abstract void submitSummaryNote(C client, P pullRequest, AnalysisDetails analysis, AnalysisSummary analysisSummary); protected abstract List getDiscussions(C client, P pullRequest); @@ -150,10 +151,9 @@ private static List findIssuesWithoutCo .collect(Collectors.toList()); } - private static Optional> loadScmPathsForIssues(PostAnalysisIssueVisitor.ComponentIssue componentIssue, - AnalysisDetails analysis) { + private static Optional> loadScmPathsForIssues(PostAnalysisIssueVisitor.ComponentIssue componentIssue) { return Optional.of(componentIssue) - .map(issue -> new ImmutablePair<>(issue, analysis.getSCMPathForIssue(issue))) + .map(issue -> new ImmutablePair<>(issue, issue.getScmPath())) .filter(pair -> pair.getRight().isPresent()) .map(pair -> new ImmutablePair<>(pair.getLeft(), pair.getRight().get())); } @@ -171,9 +171,8 @@ private static boolean isIssueFromCommitInCurrentRequest(PostAnalysisIssueVisito .isPresent(); } - private List>> findOpenSonarqubeComments(C client, P pullRequest, - U currentUser, - AnalysisDetails analysisDetails) { + private List>> findOpenSonarqubeComments(C client, P pullRequest, + U currentUser) { return getDiscussions(client, pullRequest).stream() .map(discussion -> { List commentsForDiscussion = getNotesForDiscussion(client, discussion); @@ -181,7 +180,7 @@ private List>> fin .findFirst() .filter(note -> isNoteFromCurrentUser(note, currentUser)) .filter(note -> !isResolved(client, discussion, commentsForDiscussion, currentUser)) - .map(note -> new ImmutableTriple<>(discussion, note, parseIssueDetails(client, note, analysisDetails))); + .map(note -> new ImmutableTriple<>(discussion, note, parseIssueDetails(client, note))); }) .filter(Optional::isPresent) .map(Optional::get) @@ -189,7 +188,7 @@ private List>> fin } private List closeOldDiscussionsAndExtractRemainingKeys(C client, U currentUser, - List>> openSonarqubeComments, + List>> openSonarqubeComments, List openIssues, P pullRequest) { List openIssueKeys = openIssues.stream() @@ -198,8 +197,8 @@ private List closeOldDiscussionsAndExtractRemainingKeys(C client, U curr List remainingCommentKeys = new ArrayList<>(); - for (Triple> openSonarqubeComment : openSonarqubeComments) { - Optional noteIdentifier = openSonarqubeComment.getRight(); + for (Triple> openSonarqubeComment : openSonarqubeComments) { + Optional noteIdentifier = openSonarqubeComment.getRight(); D discussion = openSonarqubeComment.getLeft(); if (noteIdentifier.isEmpty()) { continue; @@ -233,15 +232,15 @@ private void resolveOrPlaceFinalCommentOnDiscussion(C client, U currentUser, D d } - protected Optional parseIssueDetails(C client, N note, AnalysisDetails analysisDetails) { - return parseIssueDetails(client, note, analysisDetails, VIEW_IN_SONARQUBE_LABEL, NOTE_MARKDOWN_VIEW_LINK_PATTERN); + protected Optional parseIssueDetails(C client, N note) { + return parseIssueDetails(client, note, VIEW_IN_SONARQUBE_LABEL, NOTE_MARKDOWN_VIEW_LINK_PATTERN); } - protected Optional parseIssueDetails(C client, N note, AnalysisDetails analysisDetails, String label, Pattern pattern) { + protected Optional parseIssueDetails(C client, N note, String label, Pattern pattern) { try (BufferedReader reader = new BufferedReader(new StringReader(getNoteContent(client, note)))) { return reader.lines() .filter(line -> line.contains(label)) - .map(line -> parseIssueLineDetails(line, analysisDetails, pattern)) + .map(line -> parseIssueLineDetails(line, pattern)) .filter(Optional::isPresent) .map(Optional::get) .findFirst(); @@ -250,18 +249,67 @@ protected Optional parseIssueDetails(C c } } - private static Optional parseIssueLineDetails(String noteLine, AnalysisDetails analysisDetails, Pattern pattern) { + private static Optional parseIssueLineDetails(String noteLine, Pattern pattern) { Matcher identifierMatcher = pattern.matcher(noteLine); if (identifierMatcher.matches()) { - return analysisDetails.parseIssueIdFromUrl(identifierMatcher.group(1)); + return parseIssueIdFromUrl(identifierMatcher.group(1)); } else { return Optional.empty(); } } - private static boolean isCommentFromCurrentProject(Triple> comment, String projectId) { + private static Optional parseIssueIdFromUrl(String issueUrl) { + URI url = URI.create(issueUrl); + List parameters = URLEncodedUtils.parse(url, StandardCharsets.UTF_8); + Optional optionalProjectId = parameters.stream() + .filter(parameter -> "id".equals(parameter.getName())) + .map(NameValuePair::getValue) + .findFirst(); + + if (optionalProjectId.isEmpty()) { + return Optional.empty(); + } + + String projectId = optionalProjectId.get(); + + if (url.getPath().endsWith("/dashboard")) { + return Optional.of(new ProjectIssueIdentifier(projectId, "decorator-summary-comment")); + } else if (url.getPath().endsWith("security_hotspots")) { + return parameters.stream() + .filter(parameter -> "hotspots".equals(parameter.getName())) + .map(NameValuePair::getValue) + .findFirst() + .map(issueId -> new ProjectIssueIdentifier(projectId, issueId)); + } else { + return parameters.stream() + .filter(parameter -> "issues".equals(parameter.getName())) + .map(NameValuePair::getValue) + .findFirst() + .map(issueId -> new ProjectIssueIdentifier(projectId, issueId)); + } + } + + private static boolean isCommentFromCurrentProject(Triple> comment, String projectId) { return comment.getRight().filter(projectIssueIdentifier -> projectId.equals(projectIssueIdentifier.getProjectKey())).isPresent(); } + protected static class ProjectIssueIdentifier { + + private final String projectKey; + private final String issueKey; + + private ProjectIssueIdentifier(String projectKey, String issueKey) { + this.projectKey = projectKey; + this.issueKey = issueKey; + } + + public String getProjectKey() { + return projectKey; + } + + public String getIssueKey() { + return issueKey; + } + } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitor.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitor.java index 72b73fe5c..66671674e 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitor.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,12 +25,12 @@ import org.sonar.core.issue.DefaultIssue; import org.sonar.db.protobuf.DbIssues; +import javax.annotation.CheckForNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; - -import javax.annotation.CheckForNull; +import java.util.Optional; public class PostAnalysisIssueVisitor extends IssueVisitor { @@ -38,7 +38,7 @@ public class PostAnalysisIssueVisitor extends IssueVisitor { @Override public void onIssue(Component component, DefaultIssue defaultIssue) { - collectedIssues.add(new ComponentIssue(component, defaultIssue)); + collectedIssues.add(new ComponentIssue(component, new LightIssue(defaultIssue))); } public List getIssues() { @@ -50,11 +50,10 @@ public static class ComponentIssue { private final Component component; private final LightIssue issue; - ComponentIssue(Component component, DefaultIssue issue) { + ComponentIssue(Component component, LightIssue issue) { super(); this.component = component; - this.issue = (issue != null) ? new LightIssue(issue) : null; - // the null test is to please PostAnalysisIssueVisitorTest.checkAllIssuesCollected() + this.issue = issue; } public Component getComponent() { @@ -64,6 +63,13 @@ public Component getComponent() { public LightIssue getIssue() { return issue; } + + public Optional getScmPath() { + if (Component.Type.FILE == component.getType()) { + return component.getReportAttributes().getScmPath(); + } + return Optional.empty(); + } } /** @@ -85,7 +91,7 @@ public static class LightIssue { private final DbIssues.Locations locations; private final RuleKey ruleKey; - private LightIssue(DefaultIssue issue) { + LightIssue(DefaultIssue issue) { this.effortInMinutes = issue.effortInMinutes(); this.key = issue.key(); this.line = issue.getLine(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index c289e658d..a935cb1af 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -22,13 +22,8 @@ import org.sonar.api.ce.posttask.Branch; import org.sonar.api.ce.posttask.PostProjectAnalysisTask; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.config.Configuration; -import org.sonar.api.platform.Server; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; -import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; -import org.sonar.ce.task.projectanalysis.metric.MetricRepository; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.alm.setting.ALM; @@ -46,27 +41,14 @@ public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask { private static final Logger LOGGER = Loggers.get(PullRequestPostAnalysisTask.class); private final List pullRequestDecorators; - private final Server server; private final PostAnalysisIssueVisitor postAnalysisIssueVisitor; - private final MetricRepository metricRepository; - private final MeasureRepository measureRepository; - private final TreeRootHolder treeRootHolder; - private final Configuration configuration; private final DbClient dbClient; - public PullRequestPostAnalysisTask(Server server, - List pullRequestDecorators, - PostAnalysisIssueVisitor postAnalysisIssueVisitor, - MetricRepository metricRepository, MeasureRepository measureRepository, - TreeRootHolder treeRootHolder, Configuration configuration, DbClient dbClient) { + public PullRequestPostAnalysisTask(List pullRequestDecorators, + PostAnalysisIssueVisitor postAnalysisIssueVisitor, DbClient dbClient) { super(); - this.server = server; this.pullRequestDecorators = pullRequestDecorators; this.postAnalysisIssueVisitor = postAnalysisIssueVisitor; - this.metricRepository = metricRepository; - this.measureRepository = measureRepository; - this.treeRootHolder = treeRootHolder; - this.configuration = configuration; this.dbClient = dbClient; } @@ -78,7 +60,7 @@ public String getDescription() { @Override public void finished(Context context) { ProjectAnalysis projectAnalysis = context.getProjectAnalysis(); - LOGGER.debug("found " + pullRequestDecorators.size() + " pull request decorators"); + LOGGER.debug("Found " + pullRequestDecorators.size() + " pull request decorators"); Optional optionalPullRequest = projectAnalysis.getBranch().filter(branch -> Branch.Type.PULL_REQUEST == branch.getType()); if (optionalPullRequest.isEmpty()) { @@ -86,9 +68,9 @@ public void finished(Context context) { return; } - Optional optionalBranchName = optionalPullRequest.get().getName(); - if (optionalBranchName.isEmpty()) { - LOGGER.warn("No branch name has been submitted with the Pull Request. Analysis will be skipped"); + Optional optionalPullRequestId = optionalPullRequest.get().getName(); + if (optionalPullRequestId.isEmpty()) { + LOGGER.warn("No pull request ID has been submitted with the Pull Request. Analysis will be skipped"); return; } @@ -148,18 +130,14 @@ public void finished(Context context) { String commitId = revision.get(); AnalysisDetails analysisDetails = - new AnalysisDetails(new AnalysisDetails.BranchDetails(optionalBranchName.get(), commitId), - postAnalysisIssueVisitor, qualityGate, - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, - treeRootHolder), analysis, - projectAnalysis.getProject(), configuration, server.getPublicRootUrl(), - projectAnalysis.getScannerContext()); + new AnalysisDetails(optionalPullRequestId.get(), commitId, + postAnalysisIssueVisitor.getIssues(), qualityGate, projectAnalysis); PullRequestBuildStatusDecorator pullRequestDecorator = optionalPullRequestDecorator.get(); - LOGGER.info("using pull request decorator " + pullRequestDecorator.getClass().getName()); + LOGGER.info("Using pull request decorator " + pullRequestDecorator.getClass().getName()); DecorationResult decorationResult = pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); - decorationResult.getPullRequestUrl().ifPresent(pullRequestUrl -> persistPullRequestUrl(pullRequestUrl, projectAnalysis, optionalBranchName.get())); + decorationResult.getPullRequestUrl().ifPresent(pullRequestUrl -> persistPullRequestUrl(pullRequestUrl, projectAnalysis, optionalPullRequestId.get())); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java index 6243633f3..2b37c7516 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Markus Heberling, Michael Clarke + * Copyright (C) 2020-2022 Markus Heberling, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -37,11 +37,14 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DiscussionAwarePullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import org.apache.commons.lang.StringUtils; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.platform.Server; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; @@ -49,8 +52,8 @@ import org.sonar.db.protobuf.DbIssues; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -59,14 +62,17 @@ public class AzureDevOpsPullRequestDecorator extends DiscussionAwarePullRequestDecorator implements PullRequestBuildStatusDecorator { + private static final Logger logger = Loggers.get(AzureDevOpsPullRequestDecorator.class); private static final Pattern NOTE_MARKDOWN_LEGACY_SEE_LINK_PATTERN = Pattern.compile("^\\[See in SonarQube]\\((.*?)\\)$"); private final AzureDevopsClientFactory azureDevopsClientFactory; - private final FormatterFactory formatterFactory; + private final MarkdownFormatterFactory markdownFormatterFactory; - public AzureDevOpsPullRequestDecorator(Server server, ScmInfoRepository scmInfoRepository, AzureDevopsClientFactory azureDevopsClientFactory) { - super(server, scmInfoRepository); + public AzureDevOpsPullRequestDecorator(ScmInfoRepository scmInfoRepository, + AzureDevopsClientFactory azureDevopsClientFactory, + ReportGenerator reportGenerator, MarkdownFormatterFactory markdownFormatterFactory) { + super(scmInfoRepository, reportGenerator); this.azureDevopsClientFactory = azureDevopsClientFactory; - this.formatterFactory = new MarkdownFormatterFactory(); + this.markdownFormatterFactory = markdownFormatterFactory; } @Override @@ -81,14 +87,21 @@ protected AzureDevopsClient createClient(AlmSettingDto almSettingDto, ProjectAlm @Override protected Optional createFrontEndUrl(PullRequest pullRequest, AnalysisDetails analysisDetails) { - return Optional.of(pullRequest.getRepository().getRemoteUrl() + "/pullRequest/" + pullRequest.getId()); + String targetUri = pullRequest.getRepository().getRemoteUrl(); + try { + URI uri = new URI(targetUri); + targetUri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null).toString(); + } catch (URISyntaxException ex) { + logger.warn("Could not construct normalised URI for Pull Request link. Unparsed URL will be used instead", ex); + } + return Optional.of(targetUri + "/pullRequest/" + pullRequest.getId()); } @Override protected PullRequest getPullRequest(AzureDevopsClient client, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto, AnalysisDetails analysis) { int pullRequestId; try { - pullRequestId = Integer.parseInt(analysis.getBranchName()); + pullRequestId = Integer.parseInt(analysis.getPullRequestId()); } catch (NumberFormatException ex) { throw new IllegalStateException("Could not parse Pull Request Key", ex); } @@ -118,17 +131,13 @@ protected List getCommitIdsForPullRequest(AzureDevopsClient client, Pull } @Override - protected void submitPipelineStatus(AzureDevopsClient client, PullRequest pullRequest, AnalysisDetails analysis, String sonarqubeRootUrl) { + protected void submitPipelineStatus(AzureDevopsClient client, PullRequest pullRequest, AnalysisDetails analysis, AnalysisSummary analysisSummary) { try { GitPullRequestStatus gitPullRequestStatus = new GitPullRequestStatus( GitStatusStateMapper.toGitStatusState(analysis.getQualityGateStatus()), String.format("SonarQube Quality Gate - %s (%s)", analysis.getAnalysisProjectName(), analysis.getAnalysisProjectKey()), new GitStatusContext("sonarqube/qualitygate", analysis.getAnalysisProjectKey()), - String.format("%s/dashboard?id=%s&pullRequest=%s", - sonarqubeRootUrl, - URLEncoder.encode(analysis.getAnalysisProjectKey(), StandardCharsets.UTF_8.name()), - URLEncoder.encode(analysis.getBranchName(), StandardCharsets.UTF_8.name()) - ) + analysisSummary.getDashboardUrl() ); client.submitPullRequestStatus(pullRequest.getRepository().getProject().getName(), pullRequest.getRepository().getName(), pullRequest.getId(), gitPullRequestStatus); @@ -139,12 +148,11 @@ protected void submitPipelineStatus(AzureDevopsClient client, PullRequest pullRe @Override protected void submitCommitNoteForIssue(AzureDevopsClient client, PullRequest pullRequest, PostAnalysisIssueVisitor.ComponentIssue issue, String filePath, - AnalysisDetails analysis) { - String issueSummary = analysis.createAnalysisIssueSummary(issue, formatterFactory); + AnalysisDetails analysis, AnalysisIssueSummary analysisIssueSummary) { DbIssues.Locations location = issue.getIssue().getLocations(); try { - CreateCommentRequest comment = new CreateCommentRequest(issueSummary); + CreateCommentRequest comment = new CreateCommentRequest(analysisIssueSummary.format(markdownFormatterFactory)); CommentPosition fileStart = new CommentPosition( location.getTextRange().getEndLine(), location.getTextRange().getEndOffset() + 1 @@ -164,10 +172,9 @@ protected void submitCommitNoteForIssue(AzureDevopsClient client, PullRequest pu @Override - protected void submitSummaryNote(AzureDevopsClient client, PullRequest pullRequest, AnalysisDetails analysis) { + protected void submitSummaryNote(AzureDevopsClient client, PullRequest pullRequest, AnalysisDetails analysis, AnalysisSummary analysisSummary) { try { - String summaryCommentBody = analysis.createAnalysisSummary(formatterFactory); - CreateCommentRequest comment = new CreateCommentRequest(summaryCommentBody); + CreateCommentRequest comment = new CreateCommentRequest(analysisSummary.format(markdownFormatterFactory)); CreateCommentThreadRequest commentThread = new CreateCommentThreadRequest(null, Collections.singletonList(comment), CommentThreadStatus.ACTIVE); CommentThread summaryComment = client.createThread(pullRequest.getRepository().getProject().getName(), pullRequest.getRepository().getName(), pullRequest.getId(), commentThread); if (analysis.getQualityGateStatus() == QualityGate.Status.OK) { @@ -230,12 +237,12 @@ protected void resolveDiscussion(AzureDevopsClient client, CommentThread discuss } @Override - protected Optional parseIssueDetails(AzureDevopsClient client, Comment note, AnalysisDetails analysisDetails) { - Optional issueIdentifier = super.parseIssueDetails(client, note, analysisDetails); + protected Optional parseIssueDetails(AzureDevopsClient client, Comment note) { + Optional issueIdentifier = super.parseIssueDetails(client, note); if (issueIdentifier.isPresent()) { return issueIdentifier; } - return parseIssueDetails(client, note, analysisDetails, "See in SonarQube", NOTE_MARKDOWN_LEGACY_SEE_LINK_PATTERN); + return parseIssueDetails(client, note, "See in SonarQube", NOTE_MARKDOWN_LEGACY_SEE_LINK_PATTERN); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java index 704ad7f1d..709754932 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Mathias Åhsberg, Michael Clarke + * Copyright (C) 2020-2022 Mathias Åhsberg, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -26,18 +26,20 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; import com.google.common.annotations.VisibleForTesting; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.issue.Issue; -import org.sonar.api.measures.CoreMetrics; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; -import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; @@ -63,14 +65,12 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco private static final DecorationResult DEFAULT_DECORATION_RESULT = DecorationResult.builder().build(); - private static final List OPEN_ISSUE_STATUSES = - Issue.STATUSES.stream().filter(s -> !Issue.STATUS_CLOSED.equals(s) && !Issue.STATUS_RESOLVED.equals(s)) - .collect(Collectors.toList()); - private final BitbucketClientFactory bitbucketClientFactory; + private final ReportGenerator reportGenerator; - public BitbucketPullRequestDecorator(BitbucketClientFactory bitbucketClientFactory) { + public BitbucketPullRequestDecorator(BitbucketClientFactory bitbucketClientFactory, ReportGenerator reportGenerator) { this.bitbucketClientFactory = bitbucketClientFactory; + this.reportGenerator = reportGenerator; } @Override @@ -82,22 +82,20 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetail return DEFAULT_DECORATION_RESULT; } - String project = client.resolveProject(almSettingDto, projectAlmSettingDto); - String repo = client.resolveRepository(almSettingDto, projectAlmSettingDto); + AnalysisSummary analysisSummary = reportGenerator.createAnalysisSummary(analysisDetails); CodeInsightsReport codeInsightsReport = client.createCodeInsightsReport( - toReport(client, analysisDetails), - reportDescription(analysisDetails), + toReport(client, analysisSummary), + reportDescription(analysisDetails, analysisSummary), analysisDetails.getAnalysisDate().toInstant(), - analysisDetails.getDashboardUrl(), - format("%s/common/icon.png", analysisDetails.getBaseImageUrl()), - analysisDetails.getQualityGateStatus() + analysisSummary.getDashboardUrl(), + analysisSummary.getSummaryImageUrl(), + analysisDetails.getQualityGateStatus() == QualityGate.Status.OK ? ReportStatus.PASSED : ReportStatus.FAILED ); - client.uploadReport(project, repo, - analysisDetails.getCommitSha(), codeInsightsReport); + client.uploadReport(analysisDetails.getCommitSha(), codeInsightsReport, analysisDetails.getAnalysisProjectKey()); - updateAnnotations(client, project, repo, analysisDetails); + updateAnnotations(client, analysisDetails); } catch (IOException e) { LOGGER.error("Could not decorate pull request for project {}", analysisDetails.getAnalysisProjectKey(), e); } @@ -110,39 +108,35 @@ public List alm() { return Arrays.asList(ALM.BITBUCKET, ALM.BITBUCKET_CLOUD); } - private List toReport(BitbucketClient client, AnalysisDetails analysisDetails) { - Map rules = analysisDetails.countRuleByType(); - + private static List toReport(BitbucketClient client, AnalysisSummary analysisSummary) { List reportData = new ArrayList<>(); - reportData.add(reliabilityReport(rules.get(RuleType.BUG))); - reportData.add(new ReportData("Code coverage", new DataValue.Percentage(newCoverage(analysisDetails)))); - reportData.add(securityReport(rules.get(RuleType.VULNERABILITY), rules.get(RuleType.SECURITY_HOTSPOT))); - reportData.add(new ReportData("Duplication", new DataValue.Percentage(newDuplication(analysisDetails)))); - reportData.add(maintainabilityReport(rules.get(RuleType.CODE_SMELL))); - reportData.add(new ReportData("Analysis details", client.createLinkDataValue(analysisDetails.getDashboardUrl()))); + reportData.add(reliabilityReport(analysisSummary.getBugCount())); + reportData.add(new ReportData("Code coverage", new DataValue.Percentage(Optional.ofNullable(analysisSummary.getNewCoverage()).orElse(BigDecimal.ZERO)))); + reportData.add(securityReport(analysisSummary.getVulnerabilityCount(), analysisSummary.getSecurityHotspotCount())); + reportData.add(new ReportData("Duplication", new DataValue.Percentage(Optional.ofNullable(analysisSummary.getNewDuplications()).orElse(BigDecimal.ZERO)))); + reportData.add(maintainabilityReport(analysisSummary.getCodeSmellCount())); + reportData.add(new ReportData("Analysis details", client.createLinkDataValue(analysisSummary.getDashboardUrl()))); return reportData; } - private void updateAnnotations(BitbucketClient client, String project, String repo, AnalysisDetails analysisDetails) throws IOException { + private void updateAnnotations(BitbucketClient client, AnalysisDetails analysisDetails) throws IOException { final AtomicInteger chunkCounter = new AtomicInteger(0); - client.deleteAnnotations(project, repo, analysisDetails.getCommitSha()); + client.deleteAnnotations(analysisDetails.getCommitSha(), analysisDetails.getAnalysisProjectKey()); AnnotationUploadLimit uploadLimit = client.getAnnotationUploadLimit(); - Map> annotationChunks = analysisDetails.getPostAnalysisIssueVisitor().getIssues().stream() - .filter(i -> i.getComponent().getReportAttributes().getScmPath().isPresent()) - .filter(i -> i.getComponent().getType() == Component.Type.FILE) - .filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().status())) + Map> annotationChunks = analysisDetails.getScmReportableIssues().stream() .filter(i -> !(i.getIssue().type() == RuleType.SECURITY_HOTSPOT && Issue.SECURITY_HOTSPOT_RESOLUTIONS - .contains(i.getIssue().resolution()))) + .contains(i.getIssue().resolution()))) .sorted(Comparator.comparing(a -> Severity.ALL.indexOf(a.getIssue().severity()))) .map(componentIssue -> { - String path = componentIssue.getComponent().getReportAttributes().getScmPath().get(); + String path = componentIssue.getComponent().getReportAttributes().getScmPath().orElseThrow(); + AnalysisIssueSummary analysisIssueSummary = reportGenerator.createAnalysisIssueSummary(componentIssue, analysisDetails); return client.createCodeInsightsAnnotation(componentIssue.getIssue().key(), Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0), - analysisDetails.getIssueUrl(componentIssue.getIssue()), + analysisIssueSummary.getIssueUrl(), componentIssue.getIssue().getMessage(), path, toBitbucketSeverity(componentIssue.getIssue().severity()), @@ -158,7 +152,7 @@ private void updateAnnotations(BitbucketClient client, String project, String re break; } - client.uploadAnnotations(project, repo, analysisDetails.getCommitSha(), annotations); + client.uploadAnnotations(analysisDetails.getCommitSha(), annotations, analysisDetails.getAnalysisProjectKey()); } catch (BitbucketException e) { if (e.isError(BitbucketException.PAYLOAD_TOO_LARGE)) { LOGGER.warn("The annotations will be truncated since the maximum number of annotations for this report has been reached."); @@ -174,7 +168,7 @@ static boolean exceedsMaximumNumberOfAnnotations(int chunkCounter, AnnotationUpl return (chunkCounter * uploadLimit.getAnnotationBatchSize()) > uploadLimit.getTotalAllowedAnnotations(); } - private String toBitbucketSeverity(String severity) { + private static String toBitbucketSeverity(String severity) { if (severity == null) { return "LOW"; } @@ -189,7 +183,7 @@ private String toBitbucketSeverity(String severity) { } } - private String toBitbucketType(RuleType sonarqubeType) { + private static String toBitbucketType(RuleType sonarqubeType) { switch (sonarqubeType) { case SECURITY_HOTSPOT: case VULNERABILITY: @@ -203,45 +197,28 @@ private String toBitbucketType(RuleType sonarqubeType) { } } - private ReportData securityReport(Long vulnerabilities, Long hotspots) { + private static ReportData securityReport(Long vulnerabilities, Long hotspots) { String vulnerabilityDescription = vulnerabilities == 1 ? "Vulnerability" : "Vulnerabilities"; String hotspotDescription = hotspots == 1 ? "Hotspot" : "Hotspots"; String security = format("%d %s (and %d %s)", vulnerabilities, vulnerabilityDescription, hotspots, hotspotDescription); return new ReportData("Security", new DataValue.Text(security)); } - private ReportData reliabilityReport(Long bugs) { + private static ReportData reliabilityReport(Long bugs) { String description = bugs == 1 ? "Bug" : "Bugs"; return new ReportData("Reliability", new DataValue.Text(format("%d %s", bugs, description))); } - private ReportData maintainabilityReport(Long codeSmells) { + private static ReportData maintainabilityReport(Long codeSmells) { String description = codeSmells == 1 ? "Code Smell" : "Code Smells"; return new ReportData("Maintainability", new DataValue.Text(format("%d %s", codeSmells, description))); } - private String reportDescription(AnalysisDetails details) { + private static String reportDescription(AnalysisDetails details, AnalysisSummary analysisSummary) { String header = details.getQualityGateStatus() == QualityGate.Status.OK ? "Quality Gate passed" : "Quality Gate failed"; - String body = details.findFailedConditions().stream() - .map(AnalysisDetails::format) + String body = analysisSummary.getFailedQualityGateConditions().stream() .map(s -> format("- %s", s)) .collect(Collectors.joining(System.lineSeparator())); return format("%s%n%s", header, body); } - - private BigDecimal newCoverage(AnalysisDetails details) { - return details.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) - .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) - .map(QualityGate.Condition::getValue) - .map(BigDecimal::new) - .orElse(BigDecimal.ZERO); - } - - private BigDecimal newDuplication(AnalysisDetails details) { - return details.findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY) - .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) - .map(QualityGate.Condition::getValue) - .map(BigDecimal::new) - .orElse(BigDecimal.ZERO); - } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java index 3d0b23b82..197702e65 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,31 +18,79 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github; +import com.github.mc1arke.sonarqube.plugin.almclient.github.GithubClient; import com.github.mc1arke.sonarqube.plugin.almclient.github.GithubClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.Annotation; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.CheckRunDetails; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckAnnotationLevel; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckConclusionState; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.rule.Severity; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; +import java.time.Clock; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; public class GithubPullRequestDecorator implements PullRequestBuildStatusDecorator { private final GithubClientFactory githubClientFactory; + private final ReportGenerator reportGenerator; + private final MarkdownFormatterFactory markdownFormatterFactory; + private final Clock clock; - public GithubPullRequestDecorator(GithubClientFactory githubClientFactory) { + public GithubPullRequestDecorator(GithubClientFactory githubClientFactory, ReportGenerator reportGenerator, + MarkdownFormatterFactory markdownFormatterFactory, Clock clock) { this.githubClientFactory = githubClientFactory; + this.reportGenerator = reportGenerator; + this.markdownFormatterFactory = markdownFormatterFactory; + this.clock = clock; } @Override public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { + AnalysisSummary analysisSummary = reportGenerator.createAnalysisSummary(analysisDetails); + + CheckRunDetails checkRunDetails = CheckRunDetails.builder() + .withAnnotations(analysisDetails.getScmReportableIssues().stream() + .map(GithubPullRequestDecorator::createAnnotation) + .collect(Collectors.toList())) + .withCheckConclusionState(analysisDetails.getQualityGateStatus() == QualityGate.Status.OK ? CheckConclusionState.SUCCESS : CheckConclusionState.FAILURE) + .withCommitId(analysisDetails.getCommitSha()) + .withSummary(analysisSummary.format(markdownFormatterFactory)) + .withDashboardUrl(analysisSummary.getDashboardUrl()) + .withPullRequestId(Integer.parseInt(analysisDetails.getPullRequestId())) + .withStartTime(analysisDetails.getAnalysisDate().toInstant().atZone(ZoneId.of("UTC"))) + .withEndTime(ZonedDateTime.now(clock)) + .withExternalId(analysisDetails.getAnalysisId()) + .withName(String.format("%s Sonarqube Results", analysisDetails.getAnalysisProjectName())) + .withTitle("Quality Gate " + (analysisDetails.getQualityGateStatus() == QualityGate.Status.OK ? "success" : "failed")) + .build(); + try { - return githubClientFactory.createClient(projectAlmSettingDto, almSettingDto) - .createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); + GithubClient githubClient = githubClientFactory.createClient(projectAlmSettingDto, almSettingDto); + + githubClient.createCheckRun(checkRunDetails, + Optional.ofNullable(projectAlmSettingDto.getSummaryCommentEnabled()) + .orElse(false)); + + return DecorationResult.builder() + .withPullRequestUrl(githubClient.getRepositoryUrl() + "/pull/" + checkRunDetails.getPullRequestId()) + .build(); } catch (Exception ex) { throw new IllegalStateException("Could not decorate Pull Request on Github", ex); } @@ -54,4 +102,28 @@ public List alm() { return Collections.singletonList(ALM.GITHUB); } + private static Annotation createAnnotation(PostAnalysisIssueVisitor.ComponentIssue componentIssue) { + return Annotation.builder() + .withLine(Optional.ofNullable(componentIssue.getIssue().getLine()).orElse(0)) + .withScmPath(componentIssue.getScmPath().orElseThrow()) + .withMessage(Optional.ofNullable(componentIssue.getIssue().getMessage()).orElseThrow().replace("\\","\\\\").replace("\"", "\\\"")) + .withSeverity(mapToGithubAnnotationLevel(componentIssue.getIssue().severity())) + .build(); + } + + private static CheckAnnotationLevel mapToGithubAnnotationLevel(String sonarqubeSeverity) { + switch (sonarqubeSeverity) { + case Severity.INFO: + return CheckAnnotationLevel.NOTICE; + case Severity.MINOR: + case Severity.MAJOR: + return CheckAnnotationLevel.WARNING; + case Severity.CRITICAL: + case Severity.BLOCKER: + return CheckAnnotationLevel.FAILURE; + default: + throw new IllegalArgumentException("Unknown severity value: " + sonarqubeSeverity); + } + } + } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecorator.java index b55f4c097..37a22f6fc 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Markus Heberling, Michael Clarke + * Copyright (C) 2020-2022 Markus Heberling, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,19 +31,17 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DiscussionAwarePullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import org.sonar.api.ce.posttask.QualityGate; -import org.sonar.api.platform.Server; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; -import java.math.BigDecimal; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -56,12 +54,12 @@ public class GitlabMergeRequestDecorator extends DiscussionAwarePullRequestDecor "com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.pipelineId"; private final GitlabClientFactory gitlabClientFactory; - private final FormatterFactory formatterFactory; + private final MarkdownFormatterFactory formatterFactory; - public GitlabMergeRequestDecorator(Server server, ScmInfoRepository scmInfoRepository, GitlabClientFactory gitlabClientFactory) { - super(server, scmInfoRepository); + public GitlabMergeRequestDecorator(ScmInfoRepository scmInfoRepository, GitlabClientFactory gitlabClientFactory, ReportGenerator reportGenerator, MarkdownFormatterFactory formatterFactory) { + super(scmInfoRepository, reportGenerator); this.gitlabClientFactory = gitlabClientFactory; - this.formatterFactory = new MarkdownFormatterFactory(); + this.formatterFactory = formatterFactory; } @Override @@ -86,7 +84,7 @@ protected MergeRequest getPullRequest(GitlabClient client, AlmSettingDto almSett String projectId = projectAlmSettingDto.getAlmRepo(); long mergeRequestIid; try { - mergeRequestIid = Long.parseLong(analysis.getBranchName()); + mergeRequestIid = Long.parseLong(analysis.getPullRequestId()); } catch (NumberFormatException ex) { throw new IllegalStateException("Could not parse Merge Request ID", ex); } @@ -109,7 +107,7 @@ protected User getCurrentUser(GitlabClient gitlabClient) { @Override protected List getCommitIdsForPullRequest(GitlabClient gitlabClient, MergeRequest mergeRequest) { try { - return gitlabClient.getMergeRequestCommits(mergeRequest.getSourceProjectId(), mergeRequest.getIid()).stream() + return gitlabClient.getMergeRequestCommits(mergeRequest.getTargetProjectId(), mergeRequest.getIid()).stream() .map(Commit::getId) .collect(Collectors.toList()); } catch (IOException ex) { @@ -118,41 +116,33 @@ protected List getCommitIdsForPullRequest(GitlabClient gitlabClient, Mer } @Override - protected void submitPipelineStatus(GitlabClient gitlabClient, MergeRequest mergeRequest, AnalysisDetails analysis, String sonarqubeRootUrl) { + protected void submitPipelineStatus(GitlabClient gitlabClient, MergeRequest mergeRequest, AnalysisDetails analysis, + AnalysisSummary analysisSummary) { Long pipelineId = analysis.getScannerProperty(PULLREQUEST_GITLAB_PIPELINE_ID) .map(Long::parseLong) .orElse(null); - BigDecimal coverage = analysis.getCoverage().orElse(null); - try { - String dashboardUrl = String.format( - "%s/dashboard?id=%s&pullRequest=%s", - sonarqubeRootUrl, - URLEncoder.encode(analysis.getAnalysisProjectKey(), StandardCharsets.UTF_8.name()), - URLEncoder.encode(analysis.getBranchName(), StandardCharsets.UTF_8.name())); - PipelineStatus pipelineStatus = new PipelineStatus("SonarQube", "SonarQube Status", analysis.getQualityGateStatus() == QualityGate.Status.OK ? PipelineStatus.State.SUCCESS : PipelineStatus.State.FAILED, - dashboardUrl, - coverage, + analysisSummary.getDashboardUrl(), + analysisSummary.getNewCoverage(), pipelineId); - gitlabClient.setMergeRequestPipelineStatus(mergeRequest.getSourceProjectId(), analysis.getCommitSha(), pipelineStatus); + gitlabClient.setMergeRequestPipelineStatus(mergeRequest.getTargetProjectId(), analysis.getCommitSha(), pipelineStatus); } catch (IOException ex) { throw new IllegalStateException("Could not update pipeline status in Gitlab", ex); } } @Override - protected void submitCommitNoteForIssue(GitlabClient client, MergeRequest mergeRequest, PostAnalysisIssueVisitor.ComponentIssue issue, String path, AnalysisDetails analysis) { - String issueSummary = analysis.createAnalysisIssueSummary(issue, formatterFactory); - + protected void submitCommitNoteForIssue(GitlabClient client, MergeRequest mergeRequest, PostAnalysisIssueVisitor.ComponentIssue issue, String path, AnalysisDetails analysis, AnalysisIssueSummary analysisIssueSummary) { Integer line = Optional.ofNullable(issue.getIssue().getLine()).orElseThrow(() -> new IllegalStateException("No line is associated with this issue")); try { - client.addMergeRequestDiscussion(mergeRequest.getSourceProjectId(), mergeRequest.getIid(), new CommitNote(issueSummary, + client.addMergeRequestDiscussion(mergeRequest.getTargetProjectId(), mergeRequest.getIid(), + new CommitNote(analysisIssueSummary.format(formatterFactory), mergeRequest.getDiffRefs().getBaseSha(), mergeRequest.getDiffRefs().getStartSha(), mergeRequest.getDiffRefs().getHeadSha(), @@ -165,14 +155,13 @@ protected void submitCommitNoteForIssue(GitlabClient client, MergeRequest mergeR } @Override - protected void submitSummaryNote(GitlabClient client, MergeRequest mergeRequest, AnalysisDetails analysis) { + protected void submitSummaryNote(GitlabClient client, MergeRequest mergeRequest, AnalysisDetails analysis, AnalysisSummary analysisSummary) { try { - String summaryCommentBody = analysis.createAnalysisSummary(formatterFactory); - Discussion summaryComment = client.addMergeRequestDiscussion(mergeRequest.getSourceProjectId(), + Discussion summaryComment = client.addMergeRequestDiscussion(mergeRequest.getTargetProjectId(), mergeRequest.getIid(), - new MergeRequestNote(summaryCommentBody)); + new MergeRequestNote(analysisSummary.format(formatterFactory))); if (analysis.getQualityGateStatus() == QualityGate.Status.OK) { - client.resolveMergeRequestDiscussion(mergeRequest.getSourceProjectId(), mergeRequest.getIid(), summaryComment.getId()); + client.resolveMergeRequestDiscussion(mergeRequest.getTargetProjectId(), mergeRequest.getIid(), summaryComment.getId()); } } catch (IOException ex) { throw new IllegalStateException("Could not submit summary comment to Gitlab", ex); @@ -183,7 +172,7 @@ protected void submitSummaryNote(GitlabClient client, MergeRequest mergeRequest, @Override protected List getDiscussions(GitlabClient client, MergeRequest pullRequest) { try { - return client.getMergeRequestDiscussions(pullRequest.getSourceProjectId(), pullRequest.getIid()); + return client.getMergeRequestDiscussions(pullRequest.getTargetProjectId(), pullRequest.getIid()); } catch (IOException ex) { throw new IllegalStateException("Could not retrieve Merge Request discussions", ex); } @@ -220,7 +209,7 @@ protected boolean isUserNote(Note note) { @Override protected void addNoteToDiscussion(GitlabClient client, Discussion discussion, MergeRequest pullRequest, String note) { try { - client.addMergeRequestDiscussionNote(pullRequest.getSourceProjectId(), pullRequest.getIid(), discussion.getId(), note); + client.addMergeRequestDiscussionNote(pullRequest.getTargetProjectId(), pullRequest.getIid(), discussion.getId(), note); } catch (IOException ex) { throw new IllegalStateException("Could not add note to Merge Request discussion", ex); } @@ -229,7 +218,7 @@ protected void addNoteToDiscussion(GitlabClient client, Discussion discussion, M @Override protected void resolveDiscussion(GitlabClient client, Discussion discussion, MergeRequest pullRequest) { try { - client.resolveMergeRequestDiscussion(pullRequest.getSourceProjectId(), pullRequest.getIid(), discussion.getId()); + client.resolveMergeRequestDiscussion(pullRequest.getTargetProjectId(), pullRequest.getIid(), discussion.getId()); } catch (IOException ex) { throw new IllegalStateException("Could not resolve Merge Request discussion", ex); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatter.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatterFactory.java similarity index 59% rename from src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatter.java rename to src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatterFactory.java index 23080e41b..5aede3648 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatter.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,31 +18,31 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; -abstract class BaseFormatter implements Formatter { +abstract class BaseFormatterFactory implements FormatterFactory { - String childContents(Node node, FormatterFactory formatterFactory) { + protected String childContents(Node node) { StringBuilder output = new StringBuilder(); - node.getChildren().forEach(n -> output.append(formatterFor(formatterFactory, n).format(n, formatterFactory))); + node.getChildren().forEach(n -> output.append(format(n))); return output.toString(); } - private static Formatter formatterFor(FormatterFactory formatterFactory, N node) { + protected String format(Node node) { if (node instanceof Document) { - return (Formatter) formatterFactory.documentFormatter(); + return documentFormatter().format((Document) node); } else if (node instanceof Heading) { - return (Formatter) formatterFactory.headingFormatter(); + return headingFormatter().format((Heading) node); } else if (node instanceof Image) { - return (Formatter) formatterFactory.imageFormatter(); + return imageFormatter().format((Image) node); } else if (node instanceof List) { - return (Formatter) formatterFactory.listFormatter(); + return listFormatter().format((List) node); } else if (node instanceof ListItem) { - return (Formatter) formatterFactory.listItemFormatter(); + return listItemFormatter().format((ListItem) node); } else if (node instanceof Paragraph) { - return (Formatter) formatterFactory.paragraphFormatter(); + return paragraphFormatter().format((Paragraph) node); } else if (node instanceof Text) { - return (Formatter) formatterFactory.textFormatter(); + return textFormatter().format((Text) node); } else if (node instanceof Link) { - return (Formatter) formatterFactory.linkFormatter(); + return linkFormatter().format((Link) node); } else { throw new IllegalArgumentException("Unknown node type: " + node.getClass().getName()); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Formatter.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Formatter.java index 0161475a4..053e1d672 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Formatter.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Formatter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,5 +20,5 @@ public interface Formatter { - String format(N node, FormatterFactory formatterFactory); + String format(N node); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/FormatterFactory.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/FormatterFactory.java index 6267ee61f..243ecd954 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/FormatterFactory.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/FormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,6 +18,9 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; +import org.sonar.api.ce.ComputeEngineSide; + +@ComputeEngineSide public interface FormatterFactory { Formatter documentFormatter(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactory.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactory.java index 28fe72c0b..8dfaa816d 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactory.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,98 +21,62 @@ import java.util.stream.IntStream; import static com.google.common.html.HtmlEscapers.htmlEscaper; -public final class MarkdownFormatterFactory implements FormatterFactory { +public final class MarkdownFormatterFactory extends BaseFormatterFactory { @Override public Formatter documentFormatter() { - return new BaseFormatter<>() { - @Override - public String format(Document node, FormatterFactory formatterFactory) { - return childContents(node, formatterFactory); - } - }; + return this::childContents; } @Override public Formatter headingFormatter() { - return new BaseFormatter<>() { - @Override - public String format(Heading node, FormatterFactory formatterFactory) { - StringBuilder output = new StringBuilder(); - IntStream.range(0, node.getLevel()).forEach(i -> output.append("#")); - return output.append(" ").append(childContents(node, formatterFactory)).append(System.lineSeparator()) - .toString(); - } + return node -> { + StringBuilder output = new StringBuilder(); + IntStream.range(0, node.getLevel()).forEach(i -> output.append("#")); + return output.append(" ").append(childContents(node)).append(System.lineSeparator()) + .toString(); }; } @Override public Formatter imageFormatter() { - return new BaseFormatter<>() { - @Override - public String format(Image node, FormatterFactory formatterFactory) { - return String.format("![%s](%s)", node.getAltText(), node.getSource()); - } - }; + return node -> String.format("![%s](%s)", node.getAltText(), node.getSource()); } @Override public Formatter linkFormatter() { - return new BaseFormatter<>() { - @Override - public String format(Link node, FormatterFactory formatterFactory) { - return String.format("[%s](%s)", node.getChildren().isEmpty() ? node.getUrl() : childContents(node, formatterFactory), node.getUrl()); - } - }; + return node -> String.format("[%s](%s)", node.getChildren().isEmpty() ? node.getUrl() : childContents(node), node.getUrl()); } @Override public Formatter listFormatter() { - return new BaseFormatter<>() { - @Override - public String format(List node, FormatterFactory formatterFactory) { - StringBuilder output = new StringBuilder(); - node.getChildren().forEach(i -> { - if (node.getStyle() == List.Style.BULLET) { - output.append("- ").append(listItemFormatter().format((ListItem) i, formatterFactory)); - } else { - throw new IllegalArgumentException("Unknown list type: " + node.getStyle()); - } - output.append(System.lineSeparator()); - }); + return node -> { + StringBuilder output = new StringBuilder(); + node.getChildren().forEach(i -> { + if (node.getStyle() == List.Style.BULLET) { + output.append("- ").append(format(i)); + } else { + throw new IllegalArgumentException("Unknown list type: " + node.getStyle()); + } output.append(System.lineSeparator()); - return output.toString(); - } + }); + output.append(System.lineSeparator()); + return output.toString(); }; } @Override public Formatter listItemFormatter() { - return new BaseFormatter<>() { - @Override - public String format(ListItem node, FormatterFactory formatterFactory) { - return childContents(node, formatterFactory); - } - }; + return this::childContents; } @Override public Formatter paragraphFormatter() { - return new BaseFormatter<>() { - @Override - public String format(Paragraph node, FormatterFactory formatterFactory) { - return childContents(node, formatterFactory) + System.lineSeparator() + System.lineSeparator(); - } - }; + return node -> childContents(node) + System.lineSeparator() + System.lineSeparator(); } @Override public Formatter textFormatter() { - return new BaseFormatter<>() { - @Override - public String format(Text node, FormatterFactory formatterFactory) { - return htmlEscaper().escape(node.getContent()).trim(); - } - }; + return node -> htmlEscaper().escape(node.getContent()); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisIssueSummary.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisIssueSummary.java new file mode 100644 index 000000000..d7decee68 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisIssueSummary.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Image; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Link; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Node; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; +import org.apache.commons.lang.StringUtils; + +public final class AnalysisIssueSummary { + + private final String typeImageUrl; + private final String severityImageUrl; + private final String issueUrl; + private final String issueKey; + private final String projectKey; + private final Long effortInMinutes; + private final String type; + private final String message; + private final String severity; + private final String resolution; + + private AnalysisIssueSummary(Builder builder) { + this.typeImageUrl = builder.typeImageUrl; + this.severityImageUrl = builder.severityImageUrl; + this.issueUrl = builder.issueUrl; + this.issueKey = builder.issueKey; + this.projectKey = builder.projectKey; + this.effortInMinutes = builder.effortInMinutes; + this.type = builder.type; + this.message = builder.message; + this.severity = builder.severity; + this.resolution = builder.resolution; + } + + public String getTypeImageUrl() { + return typeImageUrl; + } + + public String getSeverityImageUrl() { + return severityImageUrl; + } + + public String getIssueUrl() { + return issueUrl; + } + + public String getIssueKey() { + return issueKey; + } + + public String getProjectKey() { + return projectKey; + } + + public Long getEffortInMinutes() { + return effortInMinutes; + } + + public String getType() { + return type; + } + + public String getMessage() { + return message; + } + + public String getSeverity() { + return severity; + } + + public String getResolution() { + return resolution; + } + + public String format(FormatterFactory formatterFactory) { + Long effort = getEffortInMinutes(); + Node effortNode = (null == effort ? new Text("") : new Paragraph(new Text(String.format("**Duration (min):** %s", effort)))); + + Node resolutionNode = (StringUtils.isBlank(getResolution()) ? new Text("") : new Paragraph(new Text(String.format("**Resolution:** %s", getResolution())))); + + Document document = new Document( + new Paragraph(new Text(String.format("**Type:** %s ", getType())), new Image(getType(), getTypeImageUrl())), + new Paragraph(new Text(String.format("**Severity:** %s ", getSeverity())), new Image(getSeverity(), getSeverityImageUrl())), + new Paragraph(new Text(String.format("**Message:** %s", getMessage()))), + effortNode, + resolutionNode, + new Paragraph(new Text(String.format("**Project ID:** %s **Issue ID:** %s", getProjectKey(), getIssueKey()))), + new Paragraph(new Link(getIssueUrl(), new Text("View in SonarQube"))) + ); + + return formatterFactory.documentFormatter().format(document); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String typeImageUrl; + private String severityImageUrl; + private String issueUrl; + private String issueKey; + private String projectKey; + private Long effortInMinutes; + private String type; + private String message; + private String severity; + private String resolution; + + private Builder() { + super(); + } + + public Builder withTypeImageUrl(String typeImageUrl) { + this.typeImageUrl = typeImageUrl; + return this; + } + + public Builder withSeverityImageUrl(String severityImageUrl) { + this.severityImageUrl = severityImageUrl; + return this; + } + + public Builder withIssueUrl(String issueUrl) { + this.issueUrl = issueUrl; + return this; + } + + public Builder withIssueKey(String issueKey) { + this.issueKey = issueKey; + return this; + } + + public Builder withProjectKey(String projectKey) { + this.projectKey = projectKey; + return this; + } + + public Builder withEffortInMinutes(Long effortInMinutes) { + this.effortInMinutes = effortInMinutes; + return this; + } + + public Builder withType(String type) { + this.type = type; + return this; + } + + public Builder withMessage(String message) { + this.message = message; + return this; + } + + public Builder withSeverity(String severity) { + this.severity = severity; + return this; + } + + public Builder withResolution(String resolution) { + this.resolution = resolution; + return this; + } + + public AnalysisIssueSummary build() { + return new AnalysisIssueSummary(this); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java new file mode 100644 index 000000000..a95f913ac --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Heading; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Image; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Link; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.ListItem; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public final class AnalysisSummary { + + private final String summaryImageUrl; + private final String projectKey; + + private final String statusDescription; + private final String statusImageUrl; + private final List failedQualityGateConditions; + private final String dashboardUrl; + + private final BigDecimal newCoverage; + private final BigDecimal coverage; + private final String coverageImageUrl; + + private final BigDecimal newDuplications; + private final BigDecimal duplications; + private final String duplicationsImageUrl; + + private final long totalIssueCount; + + private final long bugCount; + private final String bugImageUrl; + + private final long securityHotspotCount; + private final long vulnerabilityCount; + private final String vulnerabilityImageUrl; + + private final long codeSmellCount; + private final String codeSmellImageUrl; + + private AnalysisSummary(Builder builder) { + this.summaryImageUrl = builder.summaryImageUrl; + this.projectKey = builder.projectKey; + this.statusDescription = builder.statusDescription; + this.statusImageUrl = builder.statusImageUrl; + this.failedQualityGateConditions = builder.failedQualityGateConditions; + this.dashboardUrl = builder.dashboardUrl; + this.newCoverage = builder.newCoverage; + this.coverage = builder.coverage; + this.coverageImageUrl = builder.coverageImageUrl; + this.newDuplications = builder.newDuplications; + this.duplications = builder.duplications; + this.duplicationsImageUrl = builder.duplicationsImageUrl; + this.totalIssueCount = builder.totalIssueCount; + this.bugCount = builder.bugCount; + this.bugImageUrl = builder.bugImageUrl; + this.securityHotspotCount = builder.securityHotspotCount; + this.vulnerabilityCount = builder.vulnerabilityCount; + this.vulnerabilityImageUrl = builder.vulnerabilityImageUrl; + this.codeSmellCount = builder.codeSmellCount; + this.codeSmellImageUrl = builder.codeSmellImageUrl; + } + + public String getSummaryImageUrl() { + return summaryImageUrl; + } + + public String getProjectKey() { + return projectKey; + } + + public String getStatusDescription() { + return statusDescription; + } + + public String getStatusImageUrl() { + return statusImageUrl; + } + + public List getFailedQualityGateConditions() { + return failedQualityGateConditions; + } + + public String getDashboardUrl() { + return dashboardUrl; + } + + public BigDecimal getNewCoverage() { + return newCoverage; + } + + public BigDecimal getCoverage() { + return coverage; + } + + public String getCoverageImageUrl() { + return coverageImageUrl; + } + + public BigDecimal getNewDuplications() { + return newDuplications; + } + + public BigDecimal getDuplications() { + return duplications; + } + + public String getDuplicationsImageUrl() { + return duplicationsImageUrl; + } + + public long getTotalIssueCount() { + return totalIssueCount; + } + + public long getBugCount() { + return bugCount; + } + + public String getBugImageUrl() { + return bugImageUrl; + } + + public long getSecurityHotspotCount() { + return securityHotspotCount; + } + + public long getVulnerabilityCount() { + return vulnerabilityCount; + } + + public String getVulnerabilityImageUrl() { + return vulnerabilityImageUrl; + } + + public long getCodeSmellCount() { + return codeSmellCount; + } + + public String getCodeSmellImageUrl() { + return codeSmellImageUrl; + } + + public String format(FormatterFactory formatterFactory) { + NumberFormat decimalFormat = new DecimalFormat("#0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + + List failedConditions = getFailedQualityGateConditions(); + + Document document = new Document(new Paragraph(new Image(getStatusDescription(), getStatusImageUrl())), + failedConditions.isEmpty() ? new Text("") : + new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( + com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, + failedConditions.stream() + .map(Text::new) + .map(ListItem::new) + .toArray(ListItem[]::new)), + new Heading(1, new Text("Analysis Details")), + new Heading(2, new Text(pluralOf(getTotalIssueCount(), "Issue", "Issues"))), + new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( + com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, + new ListItem(new Image("Bug", getBugImageUrl()), + new Text(" "), + new Text(pluralOf(getBugCount(), "Bug", "Bugs"))), + new ListItem(new Image("Vulnerability", getVulnerabilityImageUrl()), + new Text(" "), + new Text(pluralOf(getVulnerabilityCount() + getSecurityHotspotCount(), "Vulnerability", "Vulnerabilities"))), + new ListItem(new Image("Code Smell", getCodeSmellImageUrl()), + new Text(" "), + new Text(pluralOf(getCodeSmellCount(), "Code Smell", "Code Smells")))), + new Heading(2, new Text("Coverage and Duplications")), + new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( + com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, + new ListItem(new Image("Coverage", getCoverageImageUrl()), + new Text(" "), new Text( + Optional.ofNullable(getNewCoverage()) + .map(decimalFormat::format) + .map(i -> i + "% Coverage") + .orElse("No coverage information") + " (" + + decimalFormat.format(Optional.ofNullable(getCoverage()).orElse(BigDecimal.valueOf(0))) + "% Estimated after merge)")), + new ListItem(new Image("Duplications", getDuplicationsImageUrl()), + new Text(" "), + new Text(Optional.ofNullable(getNewDuplications()) + .map(decimalFormat::format) + .map(i -> i + "% Duplicated Code") + .orElse("No duplication information") + " (" + decimalFormat.format(getDuplications()) + "% Estimated after merge)"))), + new Paragraph(new Text(String.format("**Project ID:** %s", getProjectKey()))), + new Paragraph(new Link(getDashboardUrl(), new Text("View in SonarQube")))); + + return formatterFactory.documentFormatter().format(document); + } + + private static String pluralOf(long value, String singleLabel, String multiLabel) { + return value + " " + (1 == value ? singleLabel : multiLabel); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String summaryImageUrl; + private String projectKey; + + private String statusDescription; + private String statusImageUrl; + private List failedQualityGateConditions; + private String dashboardUrl; + + private BigDecimal newCoverage; + private BigDecimal coverage; + private String coverageImageUrl; + + private BigDecimal newDuplications; + private BigDecimal duplications; + private String duplicationsImageUrl; + + private long totalIssueCount; + + private long bugCount; + private String bugImageUrl; + + private long securityHotspotCount; + private long vulnerabilityCount; + private String vulnerabilityImageUrl; + + private long codeSmellCount; + private String codeSmellImageUrl; + + private Builder() { + super(); + } + + public Builder withSummaryImageUrl(String summaryImageUrl) { + this.summaryImageUrl = summaryImageUrl; + return this; + } + + public Builder withProjectKey(String projectKey) { + this.projectKey = projectKey; + return this; + } + + public Builder withStatusDescription(String statusDescription) { + this.statusDescription = statusDescription; + return this; + } + + public Builder withStatusImageUrl(String statusImageUrl) { + this.statusImageUrl = statusImageUrl; + return this; + } + + public Builder withFailedQualityGateConditions(List failedQualityGateConditions) { + this.failedQualityGateConditions = failedQualityGateConditions; + return this; + } + + public Builder withDashboardUrl(String dashboardUrl) { + this.dashboardUrl = dashboardUrl; + return this; + } + + public Builder withNewCoverage(BigDecimal newCoverage) { + this.newCoverage = newCoverage; + return this; + } + + public Builder withCoverage(BigDecimal coverage) { + this.coverage = coverage; + return this; + } + + public Builder withCoverageImageUrl(String coverageImageUrl) { + this.coverageImageUrl = coverageImageUrl; + return this; + } + + public Builder withNewDuplications(BigDecimal newDuplications) { + this.newDuplications = newDuplications; + return this; + } + + public Builder withDuplications(BigDecimal duplications) { + this.duplications = duplications; + return this; + } + + public Builder withDuplicationsImageUrl(String duplicationsImageUrl) { + this.duplicationsImageUrl = duplicationsImageUrl; + return this; + } + + public Builder withTotalIssueCount(long totalIssueCount) { + this.totalIssueCount = totalIssueCount; + return this; + } + + public Builder withBugCount(long bugCount) { + this.bugCount = bugCount; + return this; + } + + public Builder withBugImageUrl(String bugImageUrl) { + this.bugImageUrl = bugImageUrl; + return this; + } + + public Builder withSecurityHotspotCount(long securityHotspotCount) { + this.securityHotspotCount = securityHotspotCount; + return this; + } + + public Builder withVulnerabilityCount(long vulnerabilityCount) { + this.vulnerabilityCount = vulnerabilityCount; + return this; + } + + public Builder withVulnerabilityImageUrl(String vulnerabilityImageUrl) { + this.vulnerabilityImageUrl = vulnerabilityImageUrl; + return this; + } + + public Builder withCodeSmellCount(long codeSmellCount) { + this.codeSmellCount = codeSmellCount; + return this; + } + + public Builder withCodeSmellImageUrl(String codeSmellImageUrl) { + this.codeSmellImageUrl = codeSmellImageUrl; + return this; + } + + public AnalysisSummary build() { + return new AnalysisSummary(this); + } + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java new file mode 100644 index 000000000..36067ab22 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report; + +import com.github.mc1arke.sonarqube.plugin.CommunityBranchPlugin; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.config.Configuration; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.platform.Server; +import org.sonar.api.rules.RuleType; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; +import org.sonar.ce.task.projectanalysis.measure.Measure; +import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; +import org.sonar.ce.task.projectanalysis.metric.MetricRepository; +import org.sonar.server.measure.Rating; + +import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ReportGenerator { + + private static final List CLOSED_ISSUE_STATUS = Arrays.asList(Issue.STATUS_CLOSED, Issue.STATUS_RESOLVED); + + private static final List COVERAGE_LEVELS = List.of(BigDecimal.valueOf(100), + BigDecimal.valueOf(90), + BigDecimal.valueOf(60), + BigDecimal.valueOf(50), + BigDecimal.valueOf(40), + BigDecimal.valueOf(25)); + + private static final List DUPLICATION_LEVELS = List.of(new DuplicationMapping(BigDecimal.valueOf(3), "3"), + new DuplicationMapping(BigDecimal.valueOf(5), "5"), + new DuplicationMapping(BigDecimal.TEN, "10"), + new DuplicationMapping(BigDecimal.valueOf(20), "20")); + + private final Server server; + private final Configuration configuration; + private final MeasureRepository measureRepository; + private final MetricRepository metricRepository; + private final TreeRootHolder treeRootHolder; + + public ReportGenerator(Server server, Configuration configuration, MeasureRepository measureRepository, MetricRepository metricRepository, TreeRootHolder treeRootHolder) { + this.server = server; + this.configuration = configuration; + this.measureRepository = measureRepository; + this.metricRepository = metricRepository; + this.treeRootHolder = treeRootHolder; + } + + public AnalysisIssueSummary createAnalysisIssueSummary(PostAnalysisIssueVisitor.ComponentIssue componentIssue, AnalysisDetails analysisDetails) { + final PostAnalysisIssueVisitor.LightIssue issue = componentIssue.getIssue(); + + String baseImageUrl = getBaseImageUrl(); + + return AnalysisIssueSummary.builder() + .withEffortInMinutes(issue.effortInMinutes()) + .withIssueKey(issue.key()) + .withIssueUrl(getIssueUrl(issue, analysisDetails)) + .withMessage(issue.getMessage()) + .withProjectKey(analysisDetails.getAnalysisProjectKey()) + .withResolution(issue.resolution()) + .withSeverity(issue.severity()) + .withSeverityImageUrl(String.format("%s/checks/Severity/%s.svg?sanitize=true", baseImageUrl, issue.severity().toLowerCase())) + .withType(issue.type().name()) + .withTypeImageUrl(String.format("%s/checks/IssueType/%s.svg?sanitize=true", baseImageUrl, issue.type().name().toLowerCase())) + .build(); + } + + public AnalysisSummary createAnalysisSummary(AnalysisDetails analysisDetails) { + BigDecimal newCoverage = analysisDetails.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY) + .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) + .map(QualityGate.Condition::getValue) + .map(BigDecimal::new) + .orElse(null); + + BigDecimal coverage = findMeasure(CoreMetrics.COVERAGE_KEY) + .map(Measure::getDoubleValue) + .map(BigDecimal::new) + .orElse(null); + + BigDecimal newDuplications = analysisDetails.findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY) + .filter(condition -> condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) + .map(QualityGate.Condition::getValue) + .map(BigDecimal::new) + .orElse(null); + + BigDecimal duplications = findMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY_KEY) + .map(Measure::getDoubleValue) + .map(BigDecimal::valueOf) + .orElse(null); + + Map issueCounts = countRuleByType(analysisDetails.getIssues()); + long issueTotal = issueCounts.values().stream().mapToLong(l -> l).sum(); + + List failedConditions = analysisDetails.findFailedQualityGateConditions(); + + String baseImageUrl = getBaseImageUrl(); + + return AnalysisSummary.builder() + .withProjectKey(analysisDetails.getAnalysisProjectKey()) + .withSummaryImageUrl(baseImageUrl + "/common/icon.png") + .withBugCount(issueCounts.get(RuleType.BUG)) + .withBugImageUrl(baseImageUrl + "/common/bug.svg?sanitize=true") + .withCodeSmellCount(issueCounts.get(RuleType.CODE_SMELL)) + .withCodeSmellImageUrl(baseImageUrl + "/common/code_smell.svg?sanitize=true") + .withCoverage(coverage) + .withNewCoverage(newCoverage) + .withCoverageImageUrl(createCoverageImage(newCoverage, baseImageUrl)) + .withDashboardUrl(getDashboardUrl(analysisDetails)) + .withDuplications(duplications) + .withDuplicationsImageUrl(createDuplicateImage(newDuplications, baseImageUrl)) + .withNewDuplications(newDuplications) + .withFailedQualityGateConditions(failedConditions.stream() + .map(ReportGenerator::formatQualityGateCondition) + .collect(Collectors.toList())) + .withStatusDescription(QualityGate.Status.OK == analysisDetails.getQualityGateStatus() ? "Passed" : "Failed") + .withStatusImageUrl(QualityGate.Status.OK == analysisDetails.getQualityGateStatus() + ? baseImageUrl + "/checks/QualityGateBadge/passed.svg?sanitize=true" + : baseImageUrl + "/checks/QualityGateBadge/failed.svg?sanitize=true") + .withTotalIssueCount(issueTotal) + .withVulnerabilityCount(issueCounts.get(RuleType.VULNERABILITY)) + .withSecurityHotspotCount(issueCounts.get(RuleType.SECURITY_HOTSPOT)) + .withVulnerabilityImageUrl(baseImageUrl + "/common/vulnerability.svg?sanitize=true") + .build(); + } + + private String getBaseImageUrl() { + return configuration.get(CommunityBranchPlugin.IMAGE_URL_BASE) + .orElse(server.getPublicRootUrl() + "/static/communityBranchPlugin") + .replaceAll("/*$", ""); + } + + private String getIssueUrl(PostAnalysisIssueVisitor.LightIssue issue, AnalysisDetails analysisDetails) { + if (issue.type() == RuleType.SECURITY_HOTSPOT) { + return String.format("%s/security_hotspots?id=%s&pullRequest=%s&hotspots=%s", server.getPublicRootUrl(), URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8), analysisDetails.getPullRequestId(), issue.key()); + } else { + return String.format("%s/project/issues?id=%s&pullRequest=%s&issues=%s&open=%s", server.getPublicRootUrl(), URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8), analysisDetails.getPullRequestId(), issue.key(), issue.key()); + } + } + + private Optional findMeasure(String metricKey) { + return measureRepository.getRawMeasure(treeRootHolder.getRoot(), metricRepository.getByKey(metricKey)); + } + + private String getDashboardUrl(AnalysisDetails analysisDetails) { + return server.getPublicRootUrl() + "/dashboard?id=" + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8) + "&pullRequest=" + analysisDetails.getPullRequestId(); + } + + private static String createCoverageImage(BigDecimal coverage, String baseImageUrl) { + if (null == coverage) { + return baseImageUrl + "/checks/CoverageChart/NoCoverageInfo.svg?sanitize=true"; + } + BigDecimal matchedLevel = BigDecimal.ZERO; + for (BigDecimal level : COVERAGE_LEVELS) { + if (coverage.compareTo(level) >= 0) { + matchedLevel = level; + break; + } + } + return baseImageUrl + "/checks/CoverageChart/" + matchedLevel + ".svg?sanitize=true"; + } + + private static String createDuplicateImage(BigDecimal duplications, String baseImageUrl) { + if (null == duplications) { + return baseImageUrl + "/checks/Duplications/NoDuplicationInfo.svg?sanitize=true"; + } + String matchedLevel = "20plus"; + for (DuplicationMapping level : DUPLICATION_LEVELS) { + if (level.getDuplicationLevel().compareTo(duplications) >= 0) { + matchedLevel = level.getImageName(); + break; + } + } + return baseImageUrl + "/checks/Duplications/" + matchedLevel + ".svg?sanitize=true"; + } + + private static String formatQualityGateCondition(QualityGate.Condition condition) { + Metric metric = CoreMetrics.getMetric(condition.getMetricKey()); + if (metric.getType() == Metric.ValueType.RATING) { + return String + .format("%s %s (%s %s)", Rating.valueOf(Integer.parseInt(condition.getValue())), metric.getName(), + condition.getOperator() == QualityGate.Operator.GREATER_THAN ? "is worse than" : + "is better than", Rating.valueOf(Integer.parseInt(condition.getErrorThreshold()))); + } else if (metric.getType() == Metric.ValueType.PERCENT) { + NumberFormat numberFormat = new DecimalFormat("#0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + return String.format("%s%% %s (%s %s%%)", numberFormat.format(new BigDecimal(condition.getValue())), + metric.getName(), + condition.getOperator() == QualityGate.Operator.GREATER_THAN ? "is greater than" : + "is less than", numberFormat.format(new BigDecimal(condition.getErrorThreshold()))); + } else { + return String.format("%s %s (%s %s)", condition.getValue(), metric.getName(), + condition.getOperator() == QualityGate.Operator.GREATER_THAN ? "is greater than" : + "is less than", condition.getErrorThreshold()); + } + } + + private static Map countRuleByType(List issues) { + return Arrays.stream(RuleType.values()).collect(Collectors.toMap(k -> k, + k -> issues.stream() + .map(PostAnalysisIssueVisitor.ComponentIssue::getIssue) + .filter(i -> !CLOSED_ISSUE_STATUS.contains(i.status())) + .filter(i -> k == i.type()) + .count())); + } + + private static class DuplicationMapping { + + private final BigDecimal duplicationLevel; + private final String imageName; + + DuplicationMapping(BigDecimal duplicationLevel, String imageName) { + this.duplicationLevel = duplicationLevel; + this.imageName = imageName; + } + + private BigDecimal getDuplicationLevel() { + return duplicationLevel; + } + + private String getImageName() { + return imageName; + } + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchAutoConfigurer.java new file mode 100644 index 000000000..9f3f92d7d --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchAutoConfigurer.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner; + +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +@ScannerSide +public interface BranchAutoConfigurer { + + Optional detectConfiguration(System2 system, ProjectBranches projectBranches); + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationFactory.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationFactory.java new file mode 100644 index 000000000..96c87afd8 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationFactory.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner; + +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.utils.MessageException; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.BranchInfo; +import org.sonar.scanner.scan.branch.BranchType; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +@ScannerSide +public class BranchConfigurationFactory { + + public BranchConfiguration createBranchConfiguration(String branchName, ProjectBranches branches) { + if (branches.isEmpty()) { + return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, null, null, null); + } + + String targetBranchName = branches.get(branchName) == null ? branches.defaultBranchName() : branchName; + return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, targetBranchName, null, null); + } + + public BranchConfiguration createPullRequestConfiguration(String pullRequestKey, String pullRequestBranch, String pullRequestBase, ProjectBranches branches) { + if (branches.isEmpty()) { + return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, null, pullRequestBase, pullRequestKey); + } + + String referenceBranch = branches.get(pullRequestBase) == null ? branches.defaultBranchName() : findReferenceBranch(pullRequestBase, branches); + return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, referenceBranch, pullRequestBase, pullRequestKey); + + } + + private static String findReferenceBranch(String targetBranch, ProjectBranches branches) { + BranchInfo target = Optional.ofNullable(branches.get(targetBranch)) + .orElseThrow(() -> MessageException.of("No branch exists in Sonarqube with the name " + targetBranch)); + + if (target.type() == BranchType.BRANCH) { + return targetBranch; + } + + String targetBranchTarget = target.branchTargetName(); + if (targetBranchTarget == null) { + throw MessageException.of(String.format("The branch '%s' of type %s does not have a target", target.name(), target.type())); + } + + return findReferenceBranch(targetBranchTarget, branches); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java index 8258e3223..5acbab8c0 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,26 +18,27 @@ */ package com.github.mc1arke.sonarqube.plugin.scanner; +import org.apache.commons.lang.StringUtils; import org.sonar.api.notifications.AnalysisWarnings; +import org.sonar.api.utils.MessageException; import org.sonar.api.utils.System2; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.core.config.ScannerProperties; import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonar.scanner.scan.branch.BranchConfigurationLoader; -import org.sonar.scanner.scan.branch.BranchInfo; -import org.sonar.scanner.scan.branch.BranchType; import org.sonar.scanner.scan.branch.DefaultBranchConfiguration; import org.sonar.scanner.scan.branch.ProjectBranches; import org.sonar.scanner.scan.branch.ProjectPullRequests; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * @author Michael Clarke @@ -55,89 +56,67 @@ public class CommunityBranchConfigurationLoader implements BranchConfigurationLo private final System2 system2; private final AnalysisWarnings analysisWarnings; + private final BranchConfigurationFactory branchConfigurationFactory; + private final List autoConfigurers; - public CommunityBranchConfigurationLoader(System2 system2, AnalysisWarnings analysisWarnings) { + public CommunityBranchConfigurationLoader(System2 system2, AnalysisWarnings analysisWarnings, + BranchConfigurationFactory branchConfigurationFactory, + List autoConfigurers) { super(); this.system2 = system2; this.analysisWarnings = analysisWarnings; + this.branchConfigurationFactory = branchConfigurationFactory; + this.autoConfigurers = autoConfigurers; } @Override public BranchConfiguration load(Map localSettings, ProjectBranches projectBranches, ProjectPullRequests pullRequests) { - localSettings = autoConfigure(localSettings); + List nonEmptyParameters = localSettings.entrySet().stream() + .filter(e -> StringUtils.isNotEmpty(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + boolean hasBranchParameters = BRANCH_ANALYSIS_PARAMETERS.stream() + .anyMatch(nonEmptyParameters::contains); + boolean hasPullRequestParameters = PULL_REQUEST_ANALYSIS_PARAMETERS.stream() + .anyMatch(nonEmptyParameters::contains); + + if (hasPullRequestParameters && hasBranchParameters) { + throw MessageException.of("sonar.pullrequest and sonar.branch parameters should not be specified in the same scan"); + } + + if (!hasBranchParameters && !hasPullRequestParameters) { + for (BranchAutoConfigurer branchAutoConfigurer : autoConfigurers) { + Optional optionalBranchConfiguration = branchAutoConfigurer.detectConfiguration(system2, projectBranches); + if (optionalBranchConfiguration.isPresent()) { + BranchConfiguration branchConfiguration = optionalBranchConfiguration.get(); + LOGGER.info("Auto detected {} configuration with source {} using {}", branchConfiguration.branchType(), branchConfiguration.branchName(), branchAutoConfigurer.getClass().getName()); + return branchConfiguration; + } + } + } if (null != localSettings.get(ScannerProperties.BRANCH_TARGET)) { //NOSONAR - purposefully checking for a deprecated parameter String warning = String.format("Property '%s' is no longer supported", ScannerProperties.BRANCH_TARGET); //NOSONAR - reporting use of deprecated parameter analysisWarnings.addUnique(warning); LOGGER.warn(warning); } - if (BRANCH_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) { - return createBranchConfiguration(localSettings.get(ScannerProperties.BRANCH_NAME), - projectBranches); - } else if (PULL_REQUEST_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) { - return createPullRequestConfiguration(localSettings.get(ScannerProperties.PULL_REQUEST_KEY), - localSettings.get(ScannerProperties.PULL_REQUEST_BRANCH), - localSettings.get(ScannerProperties.PULL_REQUEST_BASE), - projectBranches); - } - return new DefaultBranchConfiguration(); - } - - private Map autoConfigure(Map localSettings) { - Map mutableLocalSettings = new HashMap<>(localSettings); - if (Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) { - //GitLab CI auto configuration - if (system2.envVariable("CI_MERGE_REQUEST_IID") != null) { - // we are inside a merge request - Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_IID")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_KEY, v)); - Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BRANCH, v)); - Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BASE, v)); - } else { - // branch or tag - Optional.ofNullable(system2.envVariable("CI_COMMIT_REF_NAME")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.BRANCH_NAME, v)); - } + if (hasBranchParameters) { + String branch = StringUtils.trimToNull(localSettings.get(ScannerProperties.BRANCH_NAME)); + return branchConfigurationFactory.createBranchConfiguration(branch, projectBranches); } - if (Boolean.parseBoolean(system2.envVariable("TF_BUILD"))) { - Optional.ofNullable(system2.envVariable("SYSTEM_PULLREQUEST_PULLREQUESTID")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_KEY, v)); - Optional.ofNullable(system2.envVariable("SYSTEM_PULLREQUEST_SOURCEBRANCH")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BRANCH, v)); - Optional.ofNullable(system2.envVariable("SYSTEM_PULLREQUEST_TARGETBRANCH")).ifPresent( - v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BASE, v)); - } - return Collections.unmodifiableMap(mutableLocalSettings); - } - - private static BranchConfiguration createBranchConfiguration(String branchName, ProjectBranches branches) { - BranchInfo existingBranch = branches.get(branchName); - - if (null == existingBranch) { - return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, branches.defaultBranchName(), null, null); + if (hasPullRequestParameters) { + String key = Optional.ofNullable(StringUtils.trimToNull(localSettings.get(ScannerProperties.PULL_REQUEST_KEY))) + .orElseThrow(() -> MessageException.of(ScannerProperties.PULL_REQUEST_KEY + " is required for a pull request analysis")); + String branch = Optional.ofNullable(StringUtils.trimToNull(localSettings.get(ScannerProperties.PULL_REQUEST_BRANCH))) + .orElseThrow(() -> MessageException.of(ScannerProperties.PULL_REQUEST_BRANCH + " is required for a pull request analysis")); + String target = StringUtils.trimToNull(localSettings.get(ScannerProperties.PULL_REQUEST_BASE)); + return branchConfigurationFactory.createPullRequestConfiguration(key, branch, target, projectBranches); } - return new CommunityBranchConfiguration(branchName, existingBranch.type(), existingBranch.name(), null, null); - } - - private static BranchConfiguration createPullRequestConfiguration(String pullRequestKey, String pullRequestBranch, - String pullRequestBase, - ProjectBranches branches) { - if (null == pullRequestBase || pullRequestBase.isEmpty()) { - return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, branches.defaultBranchName(), - branches.defaultBranchName(), pullRequestKey); - } else { - return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, - Optional.ofNullable(branches.get(pullRequestBase)) - .map(b -> pullRequestBase) - .orElse(null), - pullRequestBase, pullRequestKey); - } + return new DefaultBranchConfiguration(); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/AzureDevopsAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/AzureDevopsAutoConfigurer.java new file mode 100644 index 000000000..c3c3c35cb --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/AzureDevopsAutoConfigurer.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class AzureDevopsAutoConfigurer implements BranchAutoConfigurer { + + private final BranchConfigurationFactory branchConfigurationFactory; + + public AzureDevopsAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system2, ProjectBranches projectBranches) { + if (!Boolean.parseBoolean(system2.envVariable("TF_BUILD"))) { + return Optional.empty(); + } + + String key = system2.envVariable("SYSTEM_PULLREQUEST_PULLREQUESTID"); + if (key != null) { + String sourceBranch = system2.envVariable("SYSTEM_PULLREQUEST_SOURCEBRANCH"); + String targetBranch = system2.envVariable("SYSTEM_PULLREQUEST_TARGETBRANCH"); + + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(key, sourceBranch, targetBranch, projectBranches)); + } + + return Optional.empty(); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/BitbucketPipelinesAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/BitbucketPipelinesAutoConfigurer.java new file mode 100644 index 000000000..678997a7e --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/BitbucketPipelinesAutoConfigurer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class BitbucketPipelinesAutoConfigurer implements BranchAutoConfigurer { + + private final BranchConfigurationFactory branchConfigurationFactory; + + public BitbucketPipelinesAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system2, ProjectBranches projectBranches) { + if (!Boolean.parseBoolean(system2.envVariable("CI"))) { + return Optional.empty(); + } + + String branch = system2.envVariable("BITBUCKET_BRANCH"); + if (StringUtils.isEmpty(branch)) { + return Optional.empty(); + } + + String prId = system2.envVariable("BITBUCKET_PR_ID"); + String targetBranch = system2.envVariable("BITBUCKET_PR_DESTINATION_BRANCH"); + if (StringUtils.isEmpty(prId)) { + return Optional.of(branchConfigurationFactory.createBranchConfiguration(branch, projectBranches)); + } else { + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(prId, branch, targetBranch, projectBranches)); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CirrusCiAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CirrusCiAutoConfigurer.java new file mode 100644 index 000000000..da420da95 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CirrusCiAutoConfigurer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class CirrusCiAutoConfigurer implements BranchAutoConfigurer { + + private final BranchConfigurationFactory branchConfigurationFactory; + + public CirrusCiAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system2, ProjectBranches projectBranches) { + if (!Boolean.parseBoolean(system2.envVariable("CIRRUS_CI"))) { + return Optional.empty(); + } + + String branch = system2.envVariable("CIRRUS_BRANCH"); + if (StringUtils.isEmpty(branch)) { + return Optional.empty(); + } + + String pullRequestId = system2.envVariable("CIRRUS_PR"); + if (StringUtils.isEmpty(pullRequestId)) { + return Optional.of(branchConfigurationFactory.createBranchConfiguration(branch, projectBranches)); + } else { + String targetBranch = system2.envVariable("CIRRUS_BASE_BRANCH"); + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(pullRequestId, branch, targetBranch, projectBranches)); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CodeMagicAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CodeMagicAutoConfigurer.java new file mode 100644 index 000000000..56f8a86b9 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CodeMagicAutoConfigurer.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class CodeMagicAutoConfigurer implements BranchAutoConfigurer { + + private final BranchConfigurationFactory branchConfigurationFactory; + + public CodeMagicAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system2, ProjectBranches projectBranches) { + if (!Boolean.parseBoolean(system2.envVariable("CI"))) { + return Optional.empty(); + } + + String sourceBranch = system2.envVariable("FCI_BRANCH"); + if (StringUtils.isEmpty(sourceBranch)) { + return Optional.empty(); + } + + if (Boolean.parseBoolean(system2.envVariable("FCI_PULL_REQUEST"))) { + String pullRequestId = system2.envVariable("FCI_PULL_REQUEST_NUMBER"); + String targetBranch = system2.envVariable("FCI_PULL_REQUEST_DEST"); + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(pullRequestId, sourceBranch, targetBranch, projectBranches)); + } else { + return Optional.of(branchConfigurationFactory.createBranchConfiguration(sourceBranch, projectBranches)); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GithubActionsAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GithubActionsAutoConfigurer.java new file mode 100644 index 000000000..d18a8e0a5 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GithubActionsAutoConfigurer.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class GithubActionsAutoConfigurer implements BranchAutoConfigurer { + + private static final String PULL_REQUEST_REF_PREFIX = "refs/pull/"; + private static final String PULL_REQUEST_REF_POSTFIX = "/merge"; + + private final BranchConfigurationFactory branchConfigurationFactory; + + public GithubActionsAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system, ProjectBranches projectBranches) { + if (!Boolean.parseBoolean(system.envVariable("GITHUB_ACTIONS"))) { + return Optional.empty(); + } + + String ref = system.envVariable("GITHUB_REF"); + if (StringUtils.isEmpty(ref)) { + return Optional.empty(); + } + + String sourceBranch = system.envVariable("GITHUB_HEAD_REF"); + if (ref.startsWith(PULL_REQUEST_REF_PREFIX) && ref.endsWith(PULL_REQUEST_REF_POSTFIX) && StringUtils.isNotEmpty(sourceBranch)) { + String key = ref.substring(PULL_REQUEST_REF_PREFIX.length(), ref.length() - PULL_REQUEST_REF_POSTFIX.length()); + String targetBranch = system.envVariable("GITHUB_BASE_REF"); + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(key, sourceBranch, targetBranch, projectBranches)); + } else { + String branch = system.envVariable("GITHUB_REF_NAME"); + return Optional.of(branchConfigurationFactory.createBranchConfiguration(branch, projectBranches)); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GitlabCiAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GitlabCiAutoConfigurer.java new file mode 100644 index 000000000..92dc904b0 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GitlabCiAutoConfigurer.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class GitlabCiAutoConfigurer implements BranchAutoConfigurer { + + private final BranchConfigurationFactory branchConfigurationFactory; + + public GitlabCiAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system2, ProjectBranches projectBranches) { + if (!Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) { + return Optional.empty(); + } + + String mergeRequestId = system2.envVariable("CI_MERGE_REQUEST_IID"); + if (StringUtils.isEmpty(mergeRequestId)) { + return Optional.of(branchConfigurationFactory.createBranchConfiguration(system2.envVariable("CI_COMMIT_REF_NAME"), projectBranches)); + } else { + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(mergeRequestId, system2.envVariable("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"), system2.envVariable("CI_MERGE_REQUEST_TARGET_BRANCH_NAME"), projectBranches)); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/JenkinsAutoConfigurer.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/JenkinsAutoConfigurer.java new file mode 100644 index 000000000..3e5c29b0f --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/JenkinsAutoConfigurer.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer; +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +import java.util.Optional; + +public class JenkinsAutoConfigurer implements BranchAutoConfigurer { + + private final BranchConfigurationFactory branchConfigurationFactory; + + public JenkinsAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) { + this.branchConfigurationFactory = branchConfigurationFactory; + } + + @Override + public Optional detectConfiguration(System2 system2, ProjectBranches projectBranches) { + if (StringUtils.isEmpty(system2.envVariable("JENKINS_HOME"))) { + return Optional.empty(); + } + + String pullRequestId = system2.envVariable("CHANGE_ID"); + if (StringUtils.isEmpty(pullRequestId)) { + return Optional.of(branchConfigurationFactory.createBranchConfiguration(system2.envVariable("BRANCH_NAME"), projectBranches)); + } else { + String changeBranch = system2.envVariable("CHANGE_BRANCH"); + String changeTarget = system2.envVariable("CHANGE_TARGET"); + return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(pullRequestId, changeBranch, changeTarget, projectBranches)); + } + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidator.java index a76646149..77ce86ebf 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -56,7 +56,7 @@ public void validate(ProjectAlmSettingDto projectAlmSettingDto, AlmSettingDto al throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Could not create Bitbucket client - " + ex.getMessage(), ex); } try { - bitbucketClient.retrieveRepository(bitbucketClient.resolveProject(almSettingDto, projectAlmSettingDto), bitbucketClient.resolveRepository(almSettingDto, projectAlmSettingDto)); + bitbucketClient.retrieveRepository(); } catch (IOException | RuntimeException ex) { throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, "Could not retrieve repository details from Bitbucket - " + ex.getMessage(), ex); } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java index 852d04345..cbce5c46e 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -118,7 +118,7 @@ public void testServerSideLoad() { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); - assertEquals(22, argumentCaptor.getAllValues().size()); + assertEquals(25, argumentCaptor.getAllValues().size()); assertEquals(Arrays.asList(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class), argumentCaptor.getAllValues().subList(0, 2)); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java index 88803eb19..93e0bf081 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/azuredevops/AzureDevopsRestClientTest.java @@ -56,7 +56,7 @@ void checkErrorThrownOnNonSuccessResponseStatus() throws IOException { assertThat(request.getMethod()).isEqualTo("post"); assertThat(request.getUri()).isEqualTo(URI.create("http://url.test/api/project/_apis/git/repositories/repo/pullRequests/101/statuses?api-version=4.1-preview")); - assertThat(request.getEntity()).usingRecursiveComparison().isEqualTo(new StringEntity("json", StandardCharsets.UTF_8)); + assertThat(request.getEntity().getContent()).hasContent("json"); } @Test @@ -79,7 +79,7 @@ void checkSubmitPullRequestStatusSubmitsCorrectContent() throws IOException { assertThat(request.getMethod()).isEqualTo("post"); assertThat(request.getUri()).isEqualTo(URI.create("http://url.test/api/project%20Id%20With%20Spaces/_apis/git/repositories/repository%20Name%20With%20Spaces/pullRequests/123/statuses?api-version=4.1-preview")); - assertThat(request.getEntity()).usingRecursiveComparison().isEqualTo(new StringEntity("json", StandardCharsets.UTF_8)); + assertThat(request.getEntity().getContent()).hasContent("json"); } @Test @@ -102,7 +102,7 @@ void checkAddCommentToThreadSubmitsCorrectContent() throws IOException { assertThat(request.getMethod()).isEqualTo("post"); assertThat(request.getUri()).isEqualTo(URI.create("http://test.url/projectId/_apis/git/repositories/repository%20Name/pullRequests/123/threads/321/comments?api-version=4.1")); - assertThat(request.getEntity()).usingRecursiveComparison().isEqualTo(new StringEntity("json", StandardCharsets.UTF_8)); + assertThat(request.getEntity().getContent()).hasContent("json"); } @Test diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClientUnitTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClientUnitTest.java index 10782d38c..aade33685 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClientUnitTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketCloudClientUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,9 +20,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.BitbucketConfiguration; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudAnnotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.cloud.CloudCreateReportRequest; import com.google.common.collect.Sets; @@ -37,7 +39,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.sonar.api.ce.posttask.QualityGate; import java.io.IOException; import java.time.Instant; @@ -67,9 +68,10 @@ public class BitbucketCloudClientUnitTest { @Before public void before() { + BitbucketConfiguration bitbucketConfiguration = new BitbucketConfiguration("project", "repository"); Call call = mock(Call.class); when(client.newCall(any())).thenReturn(call); - underTest = new BitbucketCloudClient(mapper, client); + underTest = new BitbucketCloudClient(mapper, client, bitbucketConfiguration); } @Test @@ -87,13 +89,13 @@ public void testUploadReport() throws IOException { when(mapper.writeValueAsString(report)).thenReturn("{payload}"); // when - underTest.uploadReport("project", "repository", "commit", report); + underTest.uploadReport("commit", report, "reportKey"); // then verify(client, times(2)).newCall(captor.capture()); Request request = captor.getValue(); assertEquals("PUT", request.method()); - assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/com.github.mc1arke.sonarqube", request.url().toString()); + assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/reportKey", request.url().toString()); } @Test @@ -107,13 +109,13 @@ public void testDeleteReport() throws IOException { when(call.execute()).thenReturn(response); // when - underTest.deleteExistingReport("project", "repository", "commit"); + underTest.deleteExistingReport("commit", "reportKey"); // then verify(client).newCall(captor.capture()); Request request = captor.getValue(); assertEquals("DELETE", request.method()); - assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/com.github.mc1arke.sonarqube", request.url().toString()); + assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/reportKey", request.url().toString()); } @Test @@ -132,13 +134,13 @@ public void testUploadAnnotations() throws IOException { when(mapper.writeValueAsString(any())).thenReturn("{payload}"); // when - underTest.uploadAnnotations("project", "repository", "commit", annotations); + underTest.uploadAnnotations("commit", annotations, "reportKey"); // then verify(client).newCall(captor.capture()); Request request = captor.getValue(); assertEquals("POST", request.method()); - assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString()); + assertEquals("https://api.bitbucket.org/2.0/repositories/project/repository/commit/commit/reports/reportKey/annotations", request.url().toString()); } @Test @@ -170,7 +172,7 @@ public void testUploadReportFailsWithMessage() throws IOException { when(mapper.writeValueAsString(report)).thenReturn("{payload}"); // when,then - assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report)) + assertThatThrownBy(() -> underTest.uploadReport("commit", report, "reportKey")) .isInstanceOf(BitbucketCloudException.class) .hasMessage("HTTP Status Code: 400; Message:error!") .extracting(e -> ((BitbucketCloudException) e).isError(400)) @@ -183,7 +185,7 @@ public void testUploadAnnotationsWithEmptyAnnotations() throws IOException { Set annotations = Sets.newHashSet(); // when - underTest.uploadAnnotations("project", "repository", "commit", annotations); + underTest.uploadAnnotations("commit", annotations, "reportKey"); // then verify(client, never()).newCall(any()); @@ -199,10 +201,10 @@ public void testCreateAnnotationForCloud() { // then assertTrue(annotation instanceof CloudAnnotation); assertEquals("issueKey", ((CloudAnnotation) annotation).getExternalId()); - assertEquals(12, ((CloudAnnotation) annotation).getLine()); + assertEquals(12, annotation.getLine()); assertEquals("http://localhost:9000/dashboard", ((CloudAnnotation) annotation).getLink()); - assertEquals("/path/to/file", ((CloudAnnotation) annotation).getPath()); - assertEquals("MAJOR", ((CloudAnnotation) annotation).getSeverity()); + assertEquals("/path/to/file", annotation.getPath()); + assertEquals("MAJOR", annotation.getSeverity()); assertEquals("BUG", ((CloudAnnotation) annotation).getAnnotationType()); } @@ -234,15 +236,15 @@ public void testCreateCloudReport() { // given // when - CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", QualityGate.Status.ERROR); + CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", ReportStatus.FAILED); // then assertTrue(result instanceof CloudCreateReportRequest); - assertEquals(0, ((CloudCreateReportRequest) result).getData().size()); - assertEquals("reportDescription", ((CloudCreateReportRequest) result).getDetails()); - assertEquals("dashboardUrl", ((CloudCreateReportRequest) result).getLink()); + assertEquals(0, result.getData().size()); + assertEquals("reportDescription", result.getDetails()); + assertEquals("dashboardUrl", result.getLink()); assertEquals("logoUrl", ((CloudCreateReportRequest) result).getLogoUrl()); - assertEquals("FAILED", ((CloudCreateReportRequest) result).getResult()); + assertEquals("FAILED", result.getResult()); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClientUnitTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClientUnitTest.java index 009b8d36c..13c40c380 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClientUnitTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/BitbucketServerClientUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,6 +24,7 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsAnnotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.CodeInsightsReport; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.Annotation; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.BitbucketServerConfiguration; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.server.CreateReportRequest; @@ -43,7 +44,6 @@ import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; -import org.sonar.api.ce.posttask.QualityGate; import java.io.IOException; import java.time.Instant; @@ -74,7 +74,7 @@ public class BitbucketServerClientUnitTest { @Before public void before() { BitbucketServerConfiguration - config = new BitbucketServerConfiguration("repo", "slug", "https://my-server.org", "token"); + config = new BitbucketServerConfiguration("project", "repository", "https://my-server.org"); underTest = new BitbucketServerClient(config, mapper, client); } @@ -240,13 +240,13 @@ public void testUploadReport() throws IOException { when(mapper.writeValueAsString(report)).thenReturn("{payload}"); // when - underTest.uploadReport("project", "repository", "commit", report); + underTest.uploadReport("commit", report, "reportKey"); // then verify(client).newCall(captor.capture()); Request request = captor.getValue(); assertEquals("PUT", request.method()); - assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube", request.url().toString()); + assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/reportKey", request.url().toString()); } @Test @@ -264,7 +264,7 @@ public void testUploadReportFails() throws IOException { when(mapper.writeValueAsString(report)).thenReturn("{payload}"); // when,then - assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report)) + assertThatThrownBy(() -> underTest.uploadReport("commit", report, "reportKey")) .isInstanceOf(BitbucketException.class); } @@ -295,7 +295,7 @@ public void testUploadReportFailsWithMessage() throws IOException { // when,then - assertThatThrownBy(() -> underTest.uploadReport("project", "repository", "commit", report)) + assertThatThrownBy(() -> underTest.uploadReport("commit", report, "reportKey")) .isInstanceOf(BitbucketException.class) .hasMessage("error!") .extracting(e -> ((BitbucketException) e).isError(400)) @@ -323,13 +323,13 @@ public void testUploadAnnotations() throws IOException { when(response.isSuccessful()).thenReturn(true); // when - underTest.uploadAnnotations("project", "repository", "commit", annotations); + underTest.uploadAnnotations("commit", annotations, "reportKey"); // then verify(client).newCall(captor.capture()); Request request = captor.getValue(); assertEquals("POST", request.method()); - assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString()); + assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/reportKey/annotations", request.url().toString()); try (Buffer bodyContent = new Buffer()) { request.body().writeTo(bodyContent); @@ -343,7 +343,7 @@ public void testUploadAnnotationsWithEmptyAnnotations() throws IOException { Set annotations = Sets.newHashSet(); // when - underTest.uploadAnnotations("project", "repository", "commit", annotations); + underTest.uploadAnnotations("commit", annotations, "reportKey"); // then verify(client, never()).newCall(any()); @@ -361,13 +361,13 @@ public void testDeleteAnnotations() throws IOException { when(response.isSuccessful()).thenReturn(true); // when - underTest.deleteAnnotations("project", "repository", "commit"); + underTest.deleteAnnotations("commit", "reportKey"); // then verify(client).newCall(captor.capture()); Request request = captor.getValue(); assertEquals("DELETE", request.method()); - assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/com.github.mc1arke.sonarqube/annotations", request.url().toString()); + assertEquals("https://my-server.org/rest/insights/1.0/projects/project/repos/repository/commits/commit/reports/reportKey/annotations", request.url().toString()); } @Test @@ -413,7 +413,7 @@ public void testCreateCloudReport() { // given // when - CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", QualityGate.Status.ERROR); + CodeInsightsReport result = underTest.createCodeInsightsReport(new ArrayList<>(), "reportDescription", Instant.now(), "dashboardUrl", "logoUrl", ReportStatus.FAILED); // then assertTrue(result instanceof CreateReportRequest); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactoryUnitTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactoryUnitTest.java index 76073a244..7abd10fba 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactoryUnitTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/DefaultBitbucketClientFactoryUnitTest.java @@ -1,8 +1,11 @@ package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket; +import okhttp3.Interceptor; import okhttp3.OkHttpClient; +import okhttp3.Request; import okhttp3.ResponseBody; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.sonar.api.config.internal.Encryption; import org.sonar.api.config.internal.Settings; @@ -15,6 +18,8 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class DefaultBitbucketClientFactoryUnitTest { @@ -32,7 +37,7 @@ public void testCreateClientIsCloudIfCloudConfig() throws IOException { when(builder.addInterceptor(any())).thenReturn(builder); ResponseBody responseBody = mock(ResponseBody.class); - when(responseBody.string()).thenReturn("{}"); + when(responseBody.string()).thenReturn("{\"access_token\": \"dummy\"}"); when(builder.build().newCall(any()).execute().body()).thenReturn(responseBody); Settings settings = mock(Settings.class); @@ -40,10 +45,31 @@ public void testCreateClientIsCloudIfCloudConfig() throws IOException { // when when(settings.getEncryption()).thenReturn(encryption); - BitbucketClient client = new DefaultBitbucketClientFactory(settings, () -> builder).createClient(projectAlmSettingDto, almSettingDto); + HttpClientBuilderFactory httpClientBuilderFactory = mock(HttpClientBuilderFactory.class); + when(httpClientBuilderFactory.createClientBuilder()).then(i -> builder); + BitbucketClient client = new DefaultBitbucketClientFactory(settings, httpClientBuilderFactory).createClient(projectAlmSettingDto, almSettingDto); // then assertTrue(client instanceof BitbucketCloudClient); + + ArgumentCaptor interceptorArgumentCaptor = ArgumentCaptor.forClass(Interceptor.class); + verify(builder, times(2)).addInterceptor(interceptorArgumentCaptor.capture()); + + Interceptor.Chain chain = mock(Interceptor.Chain.class); + Request request = mock(Request.class); + when(chain.request()).thenReturn(request); + Request.Builder requestBuilder = mock(Request.Builder.class); + when(requestBuilder.addHeader(any(), any())).thenReturn(requestBuilder); + when(request.newBuilder()).thenReturn(requestBuilder); + + Request request2 = mock(Request.class); + when(requestBuilder.build()).thenReturn(request2); + + interceptorArgumentCaptor.getValue().intercept(chain); + + verify(requestBuilder).addHeader("Authorization", "Bearer dummy"); + verify(requestBuilder).addHeader("Accept", "application/json"); + verify(chain).proceed(request2); } @Test @@ -61,7 +87,9 @@ public void testCreateClientIfNotCloudConfig() { // when when(settings.getEncryption()).thenReturn(encryption); - BitbucketClient client = new DefaultBitbucketClientFactory(settings, () -> mock(OkHttpClient.Builder.class, Mockito.RETURNS_DEEP_STUBS)).createClient(projectAlmSettingDto, almSettingDto); + HttpClientBuilderFactory httpClientBuilderFactory = mock(HttpClientBuilderFactory.class); + when(httpClientBuilderFactory.createClientBuilder()).then(i -> mock(OkHttpClient.Builder.class, Mockito.RETURNS_DEEP_STUBS)); + BitbucketClient client = new DefaultBitbucketClientFactory(settings, httpClientBuilderFactory).createClient(projectAlmSettingDto, almSettingDto); // then assertTrue(client instanceof BitbucketServerClient); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/HttpClientBuilderFactoryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/HttpClientBuilderFactoryTest.java new file mode 100644 index 000000000..1afb4e7d3 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/bitbucket/HttpClientBuilderFactoryTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.almclient.bitbucket; + +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HttpClientBuilderFactoryTest { + + @Test + void verifyNotSameInstanceReturnedByFactory() { + HttpClientBuilderFactory underTest = new HttpClientBuilderFactory(); + OkHttpClient.Builder builder1 = underTest.createClientBuilder(); + OkHttpClient.Builder builder2 = underTest.createClientBuilder(); + + assertThat(builder1).isNotSameAs(builder2); + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactoryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactoryTest.java index ae27b7de2..29f7f434f 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactoryTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/DefaultGithubClientFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,12 +21,12 @@ import com.github.mc1arke.sonarqube.plugin.InvalidConfigurationException; import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.GraphqlGithubClient; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.GraphqlProvider; import org.assertj.core.api.Condition; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.sonar.api.config.internal.Encryption; import org.sonar.api.config.internal.Settings; -import org.sonar.api.platform.Server; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; @@ -43,8 +43,8 @@ class DefaultGithubClientFactoryTest { private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class); private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); private final RestApplicationAuthenticationProvider restApplicationAuthenticationProvider = mock(RestApplicationAuthenticationProvider.class); - private final Server server = mock(Server.class); private final Settings settings = mock(Settings.class); + private final GraphqlProvider graphqlProvider = mock(GraphqlProvider.class); @BeforeEach public void setUp() { @@ -58,7 +58,7 @@ public void setUp() { @Test void testExceptionThrownIfUrlMissing() { when(almSettingDto.getUrl()).thenReturn(null); - DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, server, settings); + DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, settings, graphqlProvider); assertThatThrownBy(() -> underTest.createClient(projectAlmSettingDto, almSettingDto)) .isInstanceOf(InvalidConfigurationException.class) .hasMessage("No URL has been set for Github connections") @@ -68,7 +68,7 @@ void testExceptionThrownIfUrlMissing() { @Test void testExceptionThrownIfPrivateKeyMissing() { when(almSettingDto.getDecryptedPrivateKey(any())).thenReturn(null); - DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, server, settings); + DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, settings, graphqlProvider); assertThatThrownBy(() -> underTest.createClient(projectAlmSettingDto, almSettingDto)) .isInstanceOf(InvalidConfigurationException.class) .hasMessage("No private key has been set for Github connections") @@ -78,7 +78,7 @@ void testExceptionThrownIfPrivateKeyMissing() { @Test void testExceptionThrownIfAlmRepoMissing() { when(projectAlmSettingDto.getAlmRepo()).thenReturn(null); - DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, server, settings); + DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, settings, graphqlProvider); assertThatThrownBy(() -> underTest.createClient(projectAlmSettingDto, almSettingDto)) .isInstanceOf(InvalidConfigurationException.class) .hasMessage("No repository name has been set for Github connections") @@ -88,7 +88,7 @@ void testExceptionThrownIfAlmRepoMissing() { @Test void testExceptionThrownIfAppIdMissing() { when(almSettingDto.getAppId()).thenReturn(null); - DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, server, settings); + DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, settings, graphqlProvider); assertThatThrownBy(() -> underTest.createClient(projectAlmSettingDto, almSettingDto)) .isInstanceOf(InvalidConfigurationException.class) .hasMessage("No App ID has been set for Github connections") @@ -97,7 +97,7 @@ void testExceptionThrownIfAppIdMissing() { @Test void testExceptionThrownIfAuthenticationProviderThrowsException() throws IOException { - DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, server, settings); + DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, settings, graphqlProvider); when(restApplicationAuthenticationProvider.getInstallationToken(any(), any(), any(), any())).thenThrow(new IOException("dummy")); assertThatThrownBy(() -> underTest.createClient(projectAlmSettingDto, almSettingDto)) .isInstanceOf(InvalidConfigurationException.class) @@ -107,10 +107,11 @@ void testExceptionThrownIfAuthenticationProviderThrowsException() throws IOExcep @Test void testHappyPath() throws IOException { - DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, server, settings); + DefaultGithubClientFactory underTest = new DefaultGithubClientFactory(restApplicationAuthenticationProvider, settings, graphqlProvider); + when(projectAlmSettingDto.getAlmRepo()).thenReturn("alm/slug"); RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); when(restApplicationAuthenticationProvider.getInstallationToken(any(), any(), any(), any())).thenReturn(repositoryAuthenticationToken); - assertThat(underTest.createClient(projectAlmSettingDto, almSettingDto)).usingRecursiveComparison().isEqualTo(new GraphqlGithubClient(repositoryAuthenticationToken, server)); + assertThat(underTest.createClient(projectAlmSettingDto, almSettingDto)).usingRecursiveComparison().isEqualTo(new GraphqlGithubClient(graphqlProvider, "url", repositoryAuthenticationToken)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java index 501f3df8b..2f2ac14f1 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -37,7 +37,6 @@ import java.util.Arrays; import java.util.Optional; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.isNull; @@ -97,7 +96,7 @@ private void testTokenForUrl(String apiUrl, String fullUrl) throws IOException { HttpURLConnection repositoriesUrlConnection = mock(HttpURLConnection.class); doReturn(new ByteArrayInputStream( ("{\"repositories\": [{\"node_id\": \"" + expectedRepositoryId + "\", \"full_name\": \"" + projectPath + - "\", \"html_url\": \"" + expectedHtmlUrl + "\"}]}").getBytes(StandardCharsets.UTF_8))).when(repositoriesUrlConnection).getInputStream(); + "\", \"html_url\": \"" + expectedHtmlUrl + "\", \"name\": \"project\", \"owner\": {\"login\": \"owner_name\"}}]}").getBytes(StandardCharsets.UTF_8))).when(repositoriesUrlConnection).getInputStream(); doReturn(repositoriesUrlConnection).when(urlProvider).createUrlConnection("repositories_url"); doReturn(installationsUrlConnection).when(urlProvider).createUrlConnection(fullUrl); @@ -165,7 +164,7 @@ public void testTokenRetrievedPaginatedHappyPath() throws IOException { HttpURLConnection repositoriesUrlConnection = mock(HttpURLConnection.class); doReturn(new ByteArrayInputStream( ("{\"repositories\": [{\"node_id\": \"" + expectedRepositoryId + (i == 0 ? "a" : "") + "\", \"full_name\": \"" + - projectPath + (i == 0 ? "a" : "") + "\"}]}").getBytes(StandardCharsets.UTF_8))).when(repositoriesUrlConnection).getInputStream(); + projectPath + (i == 0 ? "a" : "") + "\", \"name\": \"name\", \"owner\": {\"login\": \"login\"}}]}").getBytes(StandardCharsets.UTF_8))).when(repositoriesUrlConnection).getInputStream(); doReturn(i == 0 ? "a" : null).when(repositoriesUrlConnection).getHeaderField("Link"); doReturn(repositoriesUrlConnection).when(urlProvider).createUrlConnection(i == 0 ? "repositories_url" : "https://dummy.url/path?param=dummy&page=" + (i + 1)); @@ -275,14 +274,4 @@ public void testExceptionOnNoMatchingToken() throws IOException { requestPropertyArgumentCaptor.getAllValues()); } - - @Test - public void testDefaultParameters() { - Clock clock = Clock.systemDefaultZone(); - LinkHeaderReader linkHeaderReader = mock(LinkHeaderReader.class); - assertThat(new RestApplicationAuthenticationProvider(clock, linkHeaderReader, new DefaultUrlConnectionProvider())) - .usingRecursiveComparison() - .ignoringFields("objectMapper") - .isEqualTo(new RestApplicationAuthenticationProvider(linkHeaderReader)); - } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClientTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClientTest.java index 4b025e585..8efd476c9 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClientTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v4/GraphqlGithubClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.almclient.github.RepositoryAuthenticationToken; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.Annotation; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.CheckRunDetails; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckAnnotationLevel; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckConclusionState; import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.RequestableCheckStatusState; @@ -30,24 +32,20 @@ import io.aexp.nodes.graphql.GraphQLResponseEntity; import io.aexp.nodes.graphql.GraphQLTemplate; import io.aexp.nodes.graphql.InputObject; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.issue.Issue; -import org.sonar.api.platform.Server; import org.sonar.api.rule.Severity; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.component.ReportAttributes; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -58,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; @@ -69,29 +67,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class GraphqlGithubClientTest { +class GraphqlGithubClientTest { private final GraphqlProvider graphqlProvider = mock(GraphqlProvider.class, RETURNS_DEEP_STUBS); private final Clock clock = Clock.fixed(Instant.ofEpochSecond(1234567890), ZoneId.of("UTC")); - private final Server server = mock(Server.class); - private final AnalysisDetails analysisDetails = mock(AnalysisDetails.class); @Test - public void createCheckRunExceptionOnErrorResponse() throws IOException { - when(server.getPublicRootUrl()).thenReturn("http://sonar.server/root"); - - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(new ArrayList<>()); - - when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("dummy summary"); - when(analysisDetails.getCommitSha()).thenReturn("commit SHA"); - when(analysisDetails.getAnalysisProjectKey()).thenReturn("projectKey"); - when(analysisDetails.getBranchName()).thenReturn("12345"); - when(analysisDetails.getAnalysisDate()).thenReturn(new Date(1234567890)); - when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); - + void createCheckRunExceptionOnErrorResponse() throws IOException { RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); when(repositoryAuthenticationToken.getAuthenticationToken()).thenReturn("dummyAuthToken"); when(repositoryAuthenticationToken.getRepositoryId()).thenReturn("repository ID"); @@ -105,249 +87,22 @@ public void createCheckRunExceptionOnErrorResponse() throws IOException { when(graphQLTemplate.mutate(any(), eq(CreateCheckRun.class))).thenReturn(graphQLResponseEntity); when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmRepo()).thenReturn("dummy/repo"); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUrl()).thenReturn("http://host.name"); - when(almSettingDto.getAppId()).thenReturn("app id"); - when(almSettingDto.getDecryptedPrivateKey(any())).thenReturn("private key"); - GraphqlGithubClient testCase = - new GraphqlGithubClient(graphqlProvider, clock, repositoryAuthenticationToken, server); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + new GraphqlGithubClient(graphqlProvider, "https://api.url", repositoryAuthenticationToken); + CheckRunDetails checkRunDetails = CheckRunDetails.builder().withAnnotations(List.of()).withStartTime(ZonedDateTime.now()).withEndTime(ZonedDateTime.now()).build(); + assertThatThrownBy(() -> testCase.createCheckRun(checkRunDetails, true)) .hasMessage( "An error was returned in the response from the Github API:" + System.lineSeparator() + "- Error{message='example message', locations=[]}").isExactlyInstanceOf(IllegalStateException.class); } @Test - public void createCheckRunExceptionOnInvalidIssueSeverity() { - when(server.getPublicRootUrl()).thenReturn("http://sonar.server/root"); - - ReportAttributes reportAttributes = mock(ReportAttributes.class); - when(reportAttributes.getScmPath()).thenReturn(Optional.of("path")); - - Component component = mock(Component.class); - when(component.getType()).thenReturn(Component.Type.FILE); - when(component.getReportAttributes()).thenReturn(reportAttributes); - - PostAnalysisIssueVisitor.LightIssue defaultIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(defaultIssue.severity()).thenReturn("dummy"); - when(defaultIssue.status()).thenReturn(Issue.STATUS_OPEN); - when(defaultIssue.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue.getIssue()).thenReturn(defaultIssue); - when(componentIssue.getComponent()).thenReturn(component); - - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); - - when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("dummy summary"); - when(analysisDetails.getCommitSha()).thenReturn("commit SHA"); - when(analysisDetails.getAnalysisProjectKey()).thenReturn("projectKey"); - when(analysisDetails.getBranchName()).thenReturn("branchName"); - when(analysisDetails.getAnalysisDate()).thenReturn(new Date(1234567890)); - when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); - - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUrl()).thenReturn("url"); - when(almSettingDto.getAppId()).thenReturn("app ID"); - when(almSettingDto.getDecryptedPrivateKey(any())).thenReturn("key"); - when(projectAlmSettingDto.getAlmRepo()).thenReturn("group/repo"); - - GraphqlGithubClient testCase = - new GraphqlGithubClient(graphqlProvider, clock, mock(RepositoryAuthenticationToken.class), server); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) - .hasMessage("Unknown severity value: dummy") - .isExactlyInstanceOf(IllegalArgumentException.class); - } - - @Test - public void createCheckRunHappyPathOkStatus() throws IOException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain", "http://api.target.domain/graphql"); - } - - @Test - public void createCheckRunHappyPathOkStatusTrailingSlash() throws IOException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/", "http://api.target.domain/graphql"); - } - - @Test - public void createCheckRunHappyPathOkStatusApiPath() throws IOException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api", "http://api.target.domain/api/graphql"); - } - - @Test - public void createCheckRunHappyPathOkStatusApiPathTrailingSlash() throws IOException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/", "http://api.target.domain/api/graphql"); - } - - @Test - public void createCheckRunHappyPathOkStatusV3Path() throws IOException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/v3", "http://api.target.domain/api/graphql"); - } - - @Test - public void createCheckRunHappyPathOkStatusV3PathTrailingSlash() throws IOException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/v3/", "http://api.target.domain/api/graphql"); - } - - @Test - public void createCheckRunHappyPathErrorStatus() throws IOException { - createCheckRunHappyPath(QualityGate.Status.ERROR, "http://abc.de/", "http://abc.de/graphql"); - } - - private void createCheckRunHappyPath(QualityGate.Status status, String basePath, String fullPath) throws IOException { - String[] messageInput = { - "issue 1", - "issue 2", - "issue 3", - "issue 4", - "issue 5", - "issue 6", - "issue \\ \" \\\" \"\\ \\\\ \"\" 7" - }; - - String[] messageOutput = { - "issue 1", - "issue 2", - "issue 3", - "issue 4", - "issue 5", - "issue 6", - "issue \\\\ \\\" \\\\\\\" \\\"\\\\ \\\\\\\\ \\\"\\\" 7" - }; - - when(server.getPublicRootUrl()).thenReturn("http://sonar.server/root"); - - PostAnalysisIssueVisitor.LightIssue issue1 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue1.getLine()).thenReturn(2); - when(issue1.getMessage()).thenReturn(messageInput[0]); - when(issue1.severity()).thenReturn(Severity.INFO); - when(issue1.status()).thenReturn(Issue.STATUS_OPEN); - when(issue1.resolution()).thenReturn(null); - - ReportAttributes reportAttributes = mock(ReportAttributes.class); - when(reportAttributes.getScmPath()).thenReturn(Optional.of("path/to.file")); - Component component1 = mock(Component.class); - when(component1.getReportAttributes()).thenReturn(reportAttributes); - when(component1.getType()).thenReturn(Component.Type.FILE); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue1.getComponent()).thenReturn(component1); - when(componentIssue1.getIssue()).thenReturn(issue1); - - PostAnalysisIssueVisitor.LightIssue issue2 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue2.getLine()).thenReturn(null); - when(issue2.getMessage()).thenReturn(messageInput[1]); - when(issue2.status()).thenReturn(Issue.STATUS_OPEN); - when(issue2.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue2.getComponent()).thenReturn(component1); - when(componentIssue2.getIssue()).thenReturn(issue2); - when(issue2.severity()).thenReturn(Severity.BLOCKER); - - ReportAttributes reportAttributes2 = mock(ReportAttributes.class); - when(reportAttributes2.getScmPath()).thenReturn(Optional.empty()); - Component component2 = mock(Component.class); - when(component2.getReportAttributes()).thenReturn(reportAttributes); - when(component2.getType()).thenReturn(Component.Type.FILE); - - PostAnalysisIssueVisitor.LightIssue issue3 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue3.getLine()).thenReturn(9); - when(issue3.severity()).thenReturn(Severity.CRITICAL); - when(issue3.getMessage()).thenReturn(messageInput[2]); - when(issue3.status()).thenReturn(Issue.STATUS_OPEN); - when(issue3.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue3 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue3.getComponent()).thenReturn(component2); - when(componentIssue3.getIssue()).thenReturn(issue3); - - ReportAttributes reportAttributes3 = mock(ReportAttributes.class); - when(reportAttributes3.getScmPath()).thenReturn(Optional.empty()); - Component component3 = mock(Component.class); - when(component3.getReportAttributes()).thenReturn(reportAttributes); - when(component3.getType()).thenReturn(Component.Type.PROJECT); - - PostAnalysisIssueVisitor.LightIssue issue4 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue4.getLine()).thenReturn(2); - when(issue4.severity()).thenReturn(Severity.CRITICAL); - when(issue4.getMessage()).thenReturn(messageInput[3]); - when(issue4.status()).thenReturn(Issue.STATUS_OPEN); - when(issue4.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue4 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue4.getComponent()).thenReturn(component3); - when(componentIssue4.getIssue()).thenReturn(issue4); - - PostAnalysisIssueVisitor.LightIssue issue5 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue5.getLine()).thenReturn(1999); - when(issue5.severity()).thenReturn(Severity.MAJOR); - when(issue5.getMessage()).thenReturn(messageInput[4]); - when(issue5.status()).thenReturn(Issue.STATUS_OPEN); - when(issue5.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue5 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue5.getComponent()).thenReturn(component2); - when(componentIssue5.getIssue()).thenReturn(issue5); - - PostAnalysisIssueVisitor.LightIssue issue6 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue6.getLine()).thenReturn(42); - when(issue6.severity()).thenReturn(Severity.MINOR); - when(issue6.getMessage()).thenReturn(messageInput[5]); - when(issue6.status()).thenReturn(Issue.STATUS_OPEN); - when(issue6.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue6 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue6.getComponent()).thenReturn(component2); - when(componentIssue6.getIssue()).thenReturn(issue6); - - PostAnalysisIssueVisitor.LightIssue issue7 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue7.getLine()).thenReturn(42); - when(issue7.severity()).thenReturn(Severity.MINOR); - when(issue7.getMessage()).thenReturn(messageInput[6]); - when(issue7.status()).thenReturn(Issue.STATUS_OPEN); - when(issue7.resolution()).thenReturn(null); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue7 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue7.getComponent()).thenReturn(component2); - when(componentIssue7.getIssue()).thenReturn(issue7); - - PostAnalysisIssueVisitor.LightIssue issue8 = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(issue8.getLine()).thenReturn(42); - when(issue8.severity()).thenReturn(Severity.MINOR); - when(issue8.status()).thenReturn(Issue.STATUS_RESOLVED); - when(issue8.resolution()).thenReturn(Issue.RESOLUTION_FIXED); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue8 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - when(componentIssue8.getComponent()).thenReturn(component2); - when(componentIssue8.getIssue()).thenReturn(issue8); - - List issueList = - Arrays.asList(componentIssue1, componentIssue2, componentIssue3, componentIssue4, componentIssue5, - componentIssue6, componentIssue7, componentIssue8); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(issueList); - - when(analysisDetails.getQualityGateStatus()).thenReturn(status); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("dummy summary"); - when(analysisDetails.getCommitSha()).thenReturn("commit SHA"); - when(analysisDetails.getAnalysisProjectKey()).thenReturn("projectKey"); - when(analysisDetails.getAnalysisProjectName()).thenReturn("projectName"); - when(analysisDetails.getBranchName()).thenReturn("13579"); - when(analysisDetails.getAnalysisDate()).thenReturn(new Date(1234567890)); - when(analysisDetails.getAnalysisId()).thenReturn("analysis ID"); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); - + void verifyCheckRunSubmitsCorrectAnnotations() throws IOException { RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); when(repositoryAuthenticationToken.getAuthenticationToken()).thenReturn("dummyAuthToken"); when(repositoryAuthenticationToken.getRepositoryId()).thenReturn("repository ID"); + when(repositoryAuthenticationToken.getOwnerName()).thenReturn("owner"); + when(repositoryAuthenticationToken.getRepositoryName()).thenReturn("repository"); List> inputObjectBuilders = new ArrayList<>(); List> inputObjects = new ArrayList<>(); @@ -393,7 +148,7 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, ArgumentCaptor getViewer = ArgumentCaptor.forClass(GraphQLRequestEntity.class); when(graphQLTemplate.query(getViewer.capture(), eq(Viewer.class))).thenReturn(viewerResponseEntity); - GraphQLResponseEntity getPullRequestResponseEntity = + GraphQLResponseEntity getPullRequestResponseEntity = objectMapper.readValue("{" + "\"response\": " + " {" + @@ -416,10 +171,10 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, " }"+ " }" + " }" + - "}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, GetPullRequest.class)); + "}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, GetRepository.class)); ArgumentCaptor getPullRequestRequestEntityArgumentCaptor = ArgumentCaptor.forClass(GraphQLRequestEntity.class); - when(graphQLTemplate.query(getPullRequestRequestEntityArgumentCaptor.capture(), eq(GetPullRequest.class))).thenReturn(getPullRequestResponseEntity); + when(graphQLTemplate.query(getPullRequestRequestEntityArgumentCaptor.capture(), eq(GetRepository.class))).thenReturn(getPullRequestResponseEntity); GraphQLResponseEntity minimizeCommentResponseEntity = objectMapper.readValue("{\"response\":{}}", objectMapper.getTypeFactory().constructParametricType(GraphQLResponseEntity.class, MinimizeComment.class)); @@ -435,17 +190,29 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmRepo()).thenReturn("dummy/repo"); - when(projectAlmSettingDto.getSummaryCommentEnabled()).thenReturn(true); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUrl()).thenReturn(basePath); - when(almSettingDto.getAppId()).thenReturn("app id"); - when(almSettingDto.getDecryptedPrivateKey(any())).thenReturn("private key"); + CheckRunDetails checkRunDetails = CheckRunDetails.builder() + .withAnnotations(IntStream.range(0, 30).mapToObj(i -> Annotation.builder() + .withLine(i) + .withSeverity(CheckAnnotationLevel.WARNING) + .withScmPath("scmPath" + i) + .withMessage("annotationMessage " + i) + .build()).collect(Collectors.toList())) + .withCheckConclusionState(CheckConclusionState.SUCCESS) + .withCommitId("commit-id") + .withSummary("Summary message") + .withDashboardUrl("dashboard-url") + .withStartTime(clock.instant().atZone(ZoneId.of("UTC")).minus(1, ChronoUnit.MINUTES)) + .withEndTime(clock.instant().atZone(ZoneId.of("UTC"))) + .withExternalId("external-id") + .withName("Name") + .withTitle("Title") + .withPullRequestId(999) + .build(); + GraphqlGithubClient testCase = - new GraphqlGithubClient(graphqlProvider, clock, repositoryAuthenticationToken, server); - testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); + new GraphqlGithubClient(graphqlProvider, "http://api.target.domain/api", repositoryAuthenticationToken); + testCase.createCheckRun(checkRunDetails, true); assertEquals(5, requestBuilders.size()); @@ -453,11 +220,7 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, headers.put("Authorization", "Bearer dummyAuthToken"); headers.put("Accept", "application/vnd.github.antiope-preview+json"); - - verify(requestBuilders.get(0)).url(fullPath); - verify(requestBuilders.get(0)).headers(headers); verify(requestBuilders.get(0)).requestMethod(GraphQLTemplate.GraphQLMethod.MUTATE); - verify(requestBuilders.get(0)).build(); assertEquals(requestEntities.get(0), requestEntityArgumentCaptor.getValue()); ArgumentCaptor argumentsArgumentCaptor = ArgumentCaptor.forClass(Arguments.class); @@ -468,30 +231,26 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, List> expectedAnnotationObjects = new ArrayList<>(); int position = 0; - for (int i = 0; i < issueList.size(); i++) { - if (issueList.get(i).getComponent().getType() != Component.Type.FILE || - issueList.get(i).getComponent().getReportAttributes().getScmPath().isEmpty() || - issueList.get(i).getIssue().resolution() != null) { - continue; - } - int line = (null == issueList.get(i).getIssue().getLine() ? 0 : issueList.get(i).getIssue().getLine()); - verify(inputObjectBuilders.get(position)).put("startLine", line); - verify(inputObjectBuilders.get(position)).put("endLine", line); - verify(inputObjectBuilders.get(position)).build(); + for (Annotation annotation : checkRunDetails.getAnnotations()) { + int line = annotation.getLine(); + + assertThat(inputObjectBuilders.get(position).build()) + .usingRecursiveComparison() + .isEqualTo(new InputObject.Builder<>() + .put("startLine", line) + .put("endLine", line) + .build()); position++; - String path = issueList.get(i).getComponent().getReportAttributes().getScmPath().get(); - InputObject.Builder fileBuilder = inputObjectBuilders.get(position); - verify(fileBuilder).put("path", path); - verify(fileBuilder).put("location", inputObjects.get(position - 1)); - String sonarQubeSeverity = issueList.get(i).getIssue().severity(); - verify(fileBuilder).put("annotationLevel", - sonarQubeSeverity.equals(Severity.INFO) ? CheckAnnotationLevel.NOTICE : - sonarQubeSeverity.equals(Severity.MINOR) || - sonarQubeSeverity.equals(Severity.MAJOR) ? CheckAnnotationLevel.WARNING : - CheckAnnotationLevel.FAILURE); - verify(fileBuilder).put("message", messageOutput[i]); - verify(inputObjectBuilders.get(position)).build(); + String path = annotation.getScmPath(); + InputObject fileBuilder = inputObjectBuilders.get(position).build(); + assertThat(fileBuilder).usingRecursiveComparison() + .isEqualTo(new InputObject.Builder<>() + .put("path", path) + .put("location", inputObjects.get(position - 1)) + .put("annotationLevel", annotation.getSeverity()) + .put("message", annotation.getMessage()) + .build()); expectedAnnotationObjects.add(inputObjects.get(position)); @@ -500,35 +259,36 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, assertEquals(4 + position, inputObjectBuilders.size()); - ArgumentCaptor>> annotationArgumentCaptor = ArgumentCaptor.forClass(List.class); - - verify(inputObjectBuilders.get(position)) - .put("title", "Quality Gate " + (status == QualityGate.Status.OK ? "success" : "failed")); - verify(inputObjectBuilders.get(position)).put("summary", "dummy summary"); - verify(inputObjectBuilders.get(position)).put(eq("annotations"), annotationArgumentCaptor.capture()); - verify(inputObjectBuilders.get(position)).build(); - - assertThat(annotationArgumentCaptor.getValue()).isEqualTo(expectedAnnotationObjects); - - verify(inputObjectBuilders.get(position + 1)).put("repositoryId", "repository ID"); - verify(inputObjectBuilders.get(position + 1)).put("name", "projectName Sonarqube Results"); - verify(inputObjectBuilders.get(position + 1)).put("headSha", "commit SHA"); - verify(inputObjectBuilders.get(position + 1)).put("status", RequestableCheckStatusState.COMPLETED); - verify(inputObjectBuilders.get(position + 1)).put("conclusion", status == QualityGate.Status.OK ? - CheckConclusionState.SUCCESS : - CheckConclusionState.FAILURE); - verify(inputObjectBuilders.get(position + 1)) - .put("detailsUrl", "http://sonar.server/root/dashboard?id=projectKey&pullRequest=13579"); - verify(inputObjectBuilders.get(position + 1)).put("startedAt", "1970-01-15T06:56:07Z"); - verify(inputObjectBuilders.get(position + 1)).put("completedAt", "2009-02-13T23:31:30Z"); - verify(inputObjectBuilders.get(position + 1)).put("externalId", "analysis ID"); - verify(inputObjectBuilders.get(position + 1)).put("output", inputObjects.get(position)); - verify(inputObjectBuilders.get(position + 1)).build(); + assertThat(inputObjectBuilders.get(position).build()) + .usingRecursiveComparison() + .isEqualTo(new InputObject.Builder<>() + .put("title", "Title") + .put("summary", "Summary message") + .put("annotations", expectedAnnotationObjects) + .build()); + + assertThat(inputObjectBuilders.get(position + 1).build()) + .usingRecursiveComparison() + .isEqualTo(new InputObject.Builder<>() + .put("repositoryId", "repository ID") + .put("name", "Name") + .put("headSha", "commit-id") + .put("status", RequestableCheckStatusState.COMPLETED) + .put("conclusion", CheckConclusionState.SUCCESS) + .put("detailsUrl", "dashboard-url") + .put("startedAt", "2009-02-13T23:30:30Z") + .put("completedAt", "2009-02-13T23:31:30Z") + .put("externalId", "external-id") + .put("output", inputObjects.get(position)) + .build()); + + for (int i = 0; i < 5; i++) { + verify(requestBuilders.get(i)).url("http://api.target.domain/api/graphql"); + verify(requestBuilders.get(i)).headers(headers); + verify(requestBuilders.get(i)).build(); + } // Verify GetViewer - verify(requestBuilders.get(1)).url(fullPath); - verify(requestBuilders.get(1)).headers(headers); - verify(requestBuilders.get(1)).build(); assertEquals(requestEntities.get(1), getViewer.getValue()); assertEquals( "query { viewer { login } } ", @@ -536,20 +296,14 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, ); // Verify GetPullRequest - verify(requestBuilders.get(2)).url(fullPath); - verify(requestBuilders.get(2)).headers(headers); - verify(requestBuilders.get(2)).build(); assertEquals(requestEntities.get(2), getPullRequestRequestEntityArgumentCaptor.getValue()); assertEquals( - "query { repository (owner:\"dummy\",name:\"repo\") { url pullRequest : pullRequest (number:13579) { comments : comments (first:100) { nodes" + + "query { repository (owner:\"owner\",name:\"repository\") { url pullRequest : pullRequest (number:999) { comments : comments (first:100) { nodes" + " { author { type : __typename login } id minimized : isMinimized } pageInfo { hasNextPage endCursor } } id } } } ", getPullRequestRequestEntityArgumentCaptor.getValue().getRequest() ); // Validate Minimize Comment - verify(requestBuilders.get(3)).url(fullPath); - verify(requestBuilders.get(3)).headers(headers); - verify(requestBuilders.get(3)).build(); assertEquals(requestEntities.get(3), minimizeCommentRequestEntityArgumentCaptor.getValue()); assertEquals( "mutation { minimizeComment (input:{classifier:OUTDATED,subjectId:\"MDEyOklzc3VlQ29tbWVudDE1MDE3\"}) { clientMutationId } } ", @@ -557,19 +311,16 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, ); // Validate AddComment - verify(requestBuilders.get(4)).url(fullPath); - verify(requestBuilders.get(4)).headers(headers); - verify(requestBuilders.get(4)).build(); assertEquals(requestEntities.get(4), addCommentRequestEntityArgumentCaptor.getValue()); assertEquals( - "mutation { addComment (input:{body:\"dummy summary\",subjectId:\"MDExOlB1bGxSZXF1ZXN0MzUzNDc=\"}) { clientMutationId } } ", + "mutation { addComment (input:{body:\"Summary message\",subjectId:\"MDExOlB1bGxSZXF1ZXN0MzUzNDc=\"}) { clientMutationId } } ", addCommentRequestEntityArgumentCaptor.getValue().getRequest() ); } @Test - public void checkExcessIssuesCorrectlyReported() throws IOException { + void checkExcessIssuesCorrectlyReported() throws IOException { ReportAttributes reportAttributes = mock(ReportAttributes.class); when(reportAttributes.getScmPath()).thenReturn(Optional.of("abc")); Component component = mock(Component.class); @@ -592,12 +343,9 @@ public void checkExcessIssuesCorrectlyReported() throws IOException { return componentIssue; }).collect(Collectors.toList()); - PostAnalysisIssueVisitor postAnalysisIssuesVisitor = mock(PostAnalysisIssueVisitor.class); - when(postAnalysisIssuesVisitor.getIssues()).thenReturn(issues); - AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssuesVisitor); - when(analysisDetails.getBranchName()).thenReturn("13579"); + when(analysisDetails.getScmReportableIssues()).thenReturn(issues); + when(analysisDetails.getPullRequestId()).thenReturn("13579"); when(analysisDetails.getAnalysisProjectKey()).thenReturn("projectKey"); when(analysisDetails.getAnalysisDate()).thenReturn(new Date()); @@ -624,23 +372,23 @@ public void checkExcessIssuesCorrectlyReported() throws IOException { when(graphQLTemplate.mutate(any(), eq(UpdateCheckRun.class))).thenReturn(graphQLResponseEntity2); when(graphqlProvider.createGraphQLTemplate()).thenReturn(graphQLTemplate); - Clock clock = mock(Clock.class); - when(clock.instant()).thenReturn(Instant.now()); + Clock clock = Clock.fixed(Instant.now(), ZoneId.of("UTC")); RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); when(repositoryAuthenticationToken.getAuthenticationToken()).thenReturn("dummy"); - Server server = mock(Server.class); - - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmRepo()).thenReturn("dummy/repo"); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUrl()).thenReturn("http://host.name"); - when(almSettingDto.getAppId()).thenReturn("app id"); - when(almSettingDto.getDecryptedClientSecret(any())).thenReturn("private key"); + CheckRunDetails checkRunDetails = mock(CheckRunDetails.class); + when(checkRunDetails.getAnnotations()).thenReturn(IntStream.range(0, 120).mapToObj(i -> Annotation.builder() + .withLine(i).withMessage("message " + i) + .withSeverity(CheckAnnotationLevel.NOTICE) + .withScmPath("path " + i) + .build()) + .collect(Collectors.toList())); + when(checkRunDetails.getStartTime()).thenReturn(clock.instant().atZone(ZoneId.of("UTC"))); + when(checkRunDetails.getEndTime()).thenReturn(clock.instant().atZone(ZoneId.of("UTC"))); - GraphqlGithubClient testCase = new GraphqlGithubClient(graphqlProvider, clock, repositoryAuthenticationToken, server); - testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); + GraphqlGithubClient testCase = new GraphqlGithubClient(graphqlProvider, "https://api.url/path", repositoryAuthenticationToken); + testCase.createCheckRun(checkRunDetails, false); ArgumentCaptor> classArgumentCaptor = ArgumentCaptor.forClass(Class.class); verify(graphQLTemplate, times(3)).mutate(any(GraphQLRequestEntity.class), classArgumentCaptor.capture()); @@ -654,39 +402,4 @@ public void checkExcessIssuesCorrectlyReported() throws IOException { assertThat(annotationsArgumentCaptor.getAllValues().get(2)).hasSize(20); } - @Test - public void checkCorrectDefaultValuesInjected() { - Clock clock = Clock.systemDefaultZone(); - RepositoryAuthenticationToken repositoryAuthenticationToken = mock(RepositoryAuthenticationToken.class); - assertThat(new GraphqlGithubClient(repositoryAuthenticationToken, server)).usingRecursiveComparison() - .isEqualTo(new GraphqlGithubClient(new DefaultGraphqlProvider(), clock, - repositoryAuthenticationToken, server)); - } - - @Test - public void checkExceptionThrownOnMissingUrl() { - AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - - GraphqlGithubClient underTest = new GraphqlGithubClient(mock(RepositoryAuthenticationToken.class), mock(Server.class)); - assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("No URL has been set for Github connections"); - } - - @Test - public void checkExceptionThrownOnMissingRepoPath() { - AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUrl()).thenReturn("url"); - when(almSettingDto.getDecryptedPrivateKey(any())).thenReturn("private key"); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - - GraphqlGithubClient underTest = new GraphqlGithubClient(mock(RepositoryAuthenticationToken.class), mock(Server.class)); - assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("No repository name has been set for Github connections"); - } - } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/GitlabRestClientTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/GitlabRestClientTest.java index 470b2fcd4..972159f37 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/GitlabRestClientTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/gitlab/GitlabRestClientTest.java @@ -56,7 +56,7 @@ void checkErrorThrownOnNonSuccessResponseStatus() throws IOException { assertThat(request.getRequestLine().getMethod()).isEqualTo("POST"); assertThat(request.getRequestLine().getUri()).isEqualTo("http://url.test/api/projects/101/merge_requests/99/discussions"); - assertThat(request.getEntity()).usingRecursiveComparison().isEqualTo(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("body", "note")), StandardCharsets.UTF_8)); + assertThat(request.getEntity().getContent()).hasContent("body=note"); } @Test @@ -81,7 +81,7 @@ void checkCorrectEncodingUsedOnMergeRequestDiscussion() throws IOException { assertThat(request.getRequestLine().getMethod()).isEqualTo("POST"); assertThat(request.getRequestLine().getUri()).isEqualTo("http://api.url/projects/123/merge_requests/321/discussions"); - assertThat(request.getEntity()).usingRecursiveComparison().isEqualTo(new UrlEncodedFormEntity(List.of(new BasicNameValuePair("body", "Merge request note")), StandardCharsets.UTF_8)); + assertThat(request.getEntity().getContent()).hasContent("body=Merge+request+note"); } } \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java index 8a89536b2..f9e0d9c3b 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityReportAnalysisComponentProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -32,7 +32,7 @@ public class CommunityReportAnalysisComponentProviderTest { @Test public void testGetComponents() { List result = new CommunityReportAnalysisComponentProvider().getComponents(); - assertEquals(13, result.size()); + assertEquals(18, result.size()); assertEquals(CommunityBranchLoaderDelegate.class, result.get(0)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java index c1fd36d48..fcd644a95 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/AnalysisDetailsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,896 +18,219 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; -import com.github.mc1arke.sonarqube.plugin.CommunityBranchPlugin; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Formatter; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Heading; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Image; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Link; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.ListItem; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; -import org.junit.Test; -import org.mockito.ArgumentCaptor; +import org.junit.jupiter.api.Test; import org.sonar.api.ce.posttask.Analysis; +import org.sonar.api.ce.posttask.PostProjectAnalysisTask; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.ce.posttask.ScannerContext; -import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.rules.RuleType; import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; -import org.sonar.ce.task.projectanalysis.measure.Measure; -import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; -import org.sonar.ce.task.projectanalysis.metric.Metric; -import org.sonar.ce.task.projectanalysis.metric.MetricRepository; +import org.sonar.ce.task.projectanalysis.component.ReportAttributes; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class AnalysisDetailsTest { +class AnalysisDetailsTest { @Test - public void testGetBranchName() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("branchName").when(branchDetails).getBranchName(); - - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - QualityGate qualityGate = mock(QualityGate.class); - Analysis analysis = mock(Analysis.class); - Project project = mock(Project.class); - Configuration configuration = mock(Configuration.class); - ScannerContext scannerContext = mock(ScannerContext.class); - - AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null, scannerContext); - - assertEquals("branchName", testCase.getBranchName()); - } - - @Test - public void testGetCommitSha() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("commitId").when(branchDetails).getCommitId(); - - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - QualityGate qualityGate = mock(QualityGate.class); - Analysis analysis = mock(Analysis.class); - Project project = mock(Project.class); - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); - - AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null, scannerContext); - - assertEquals("commitId", testCase.getCommitSha()); - } - - @Test - public void testGetQualityGateStatus() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); + void shouldReturnStatusFromQualityGate() { QualityGate qualityGate = mock(QualityGate.class); doReturn(QualityGate.Status.ERROR).when(qualityGate).getStatus(); - Analysis analysis = mock(Analysis.class); - Project project = mock(Project.class); - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); + PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null, scannerContext); + new AnalysisDetails("pullRequestKey", "commitHash", new ArrayList<>(), qualityGate, projectAnalysis); assertEquals(QualityGate.Status.ERROR, testCase.getQualityGateStatus()); } @Test - public void testGetAnalysisDate() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); + void shouldGetDateFromAnalysis() { QualityGate qualityGate = mock(QualityGate.class); Analysis analysis = mock(Analysis.class); + PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); + doReturn(Optional.of(analysis)).when(projectAnalysis).getAnalysis(); doReturn(new Date()).when(analysis).getDate(); - Project project = mock(Project.class); - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null, scannerContext); + new AnalysisDetails("pullRequestKey", "commitHash", new ArrayList<>(), qualityGate, projectAnalysis); assertEquals(analysis.getDate(), testCase.getAnalysisDate()); } @Test - public void testGetAnalysisId() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); + void shouldGetIdFromAnalysis() { QualityGate qualityGate = mock(QualityGate.class); Analysis analysis = mock(Analysis.class); + PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); + doReturn(Optional.of(analysis)).when(projectAnalysis).getAnalysis(); doReturn("Analysis ID").when(analysis).getAnalysisUuid(); - Project project = mock(Project.class); - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null, scannerContext); + new AnalysisDetails("pullRequestKey", "commitHash", new ArrayList<>(), qualityGate, projectAnalysis); assertEquals("Analysis ID", testCase.getAnalysisId()); } @Test - public void testGetAnalysisProjectKey() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); + void shouldGetProjectKeyFromUnderlyingProject() { QualityGate qualityGate = mock(QualityGate.class); - Analysis analysis = mock(Analysis.class); + PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); Project project = mock(Project.class); - doReturn("Project Key").when(project).getKey(); - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); + when(project.getKey()).thenReturn("Project Key"); + when(projectAnalysis.getProject()).thenReturn(project); AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, null, scannerContext); + new AnalysisDetails("pullRequestKey", "commitHash", new ArrayList<>(), qualityGate, projectAnalysis); assertEquals("Project Key", testCase.getAnalysisProjectKey()); } @Test - public void testCreateAnalysisSummary() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("5").when(branchDetails).getBranchName(); - - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - doReturn(treeRootHolder).when(measuresHolder).getTreeRootHolder(); - - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - PostAnalysisIssueVisitor.LightIssue issue1 = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_CLOSED).when(issue1).status(); - - PostAnalysisIssueVisitor.LightIssue issue2 = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_OPEN).when(issue2).status(); - doReturn(RuleType.BUG).when(issue2).type(); - - PostAnalysisIssueVisitor.LightIssue issue3 = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_OPEN).when(issue3).status(); - doReturn(RuleType.SECURITY_HOTSPOT).when(issue3).type(); - - PostAnalysisIssueVisitor.LightIssue issue4 = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_OPEN).when(issue4).status(); - doReturn(RuleType.CODE_SMELL).when(issue4).type(); - - PostAnalysisIssueVisitor.LightIssue issue5 = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_OPEN).when(issue5).status(); - doReturn(RuleType.VULNERABILITY).when(issue5).type(); - - PostAnalysisIssueVisitor.LightIssue issue6 = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_OPEN).when(issue6).status(); - doReturn(RuleType.BUG).when(issue6).type(); - - doReturn(Stream.of(issue1, issue2, issue3, issue4, issue5, issue6).map(i -> { - PostAnalysisIssueVisitor.ComponentIssue componentIssue = - mock(PostAnalysisIssueVisitor.ComponentIssue.class); - doReturn(i).when(componentIssue).getIssue(); - return componentIssue; - }).collect(Collectors.toList())).when(postAnalysisIssueVisitor).getIssues(); - - QualityGate.Condition condition1 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.ERROR).when(condition1).getStatus(); - doReturn(CoreMetrics.LINES_TO_COVER.getKey()).when(condition1).getMetricKey(); - doReturn("12").when(condition1).getValue(); - doReturn(QualityGate.Operator.LESS_THAN).when(condition1).getOperator(); - doReturn("20").when(condition1).getErrorThreshold(); - - QualityGate.Condition condition2 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.ERROR).when(condition2).getStatus(); - doReturn(CoreMetrics.CODE_SMELLS.getKey()).when(condition2).getMetricKey(); - doReturn("2").when(condition2).getValue(); - doReturn(QualityGate.Operator.GREATER_THAN).when(condition2).getOperator(); - doReturn("0").when(condition2).getErrorThreshold(); - - QualityGate.Condition condition3 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.ERROR).when(condition3).getStatus(); - doReturn(CoreMetrics.LINE_COVERAGE.getKey()).when(condition3).getMetricKey(); - doReturn("68").when(condition3).getValue(); - doReturn(QualityGate.Operator.LESS_THAN).when(condition3).getOperator(); - doReturn("80").when(condition3).getErrorThreshold(); - - QualityGate.Condition condition4 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.ERROR).when(condition4).getStatus(); - doReturn(CoreMetrics.NEW_SECURITY_RATING.getKey()).when(condition4).getMetricKey(); - doReturn("5").when(condition4).getValue(); - doReturn(QualityGate.Operator.GREATER_THAN).when(condition4).getOperator(); - doReturn("4").when(condition4).getErrorThreshold(); - - QualityGate.Condition condition5 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.ERROR).when(condition5).getStatus(); - doReturn(CoreMetrics.RELIABILITY_RATING.getKey()).when(condition5).getMetricKey(); - doReturn("1").when(condition5).getValue(); - doReturn(QualityGate.Operator.LESS_THAN).when(condition5).getOperator(); - doReturn("3").when(condition5).getErrorThreshold(); - - QualityGate.Condition condition6 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.ERROR).when(condition6).getStatus(); - doReturn(CoreMetrics.BRANCH_COVERAGE.getKey()).when(condition6).getMetricKey(); - doReturn("16").when(condition6).getValue(); - doReturn(QualityGate.Operator.GREATER_THAN).when(condition6).getOperator(); - doReturn("15").when(condition6).getErrorThreshold(); - - QualityGate.Condition condition7 = mock(QualityGate.Condition.class); - doReturn(QualityGate.EvaluationStatus.OK).when(condition7).getStatus(); - doReturn(CoreMetrics.NEW_BUGS.getKey()).when(condition7).getMetricKey(); - doReturn("0").when(condition7).getValue(); - doReturn(QualityGate.Operator.LESS_THAN).when(condition7).getOperator(); - doReturn("1").when(condition7).getErrorThreshold(); - + void shouldGetProjectNameFromUnderlyingProject() { QualityGate qualityGate = mock(QualityGate.class); - doReturn(Arrays.asList(condition1, condition2, condition3, condition4, condition5, condition6, condition7)) - .when(qualityGate).getConditions(); - - Analysis analysis = mock(Analysis.class); + PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); Project project = mock(Project.class); - doReturn("Project Key").when(project).getKey(); - - Component rootComponent = mock(Component.class); - doReturn(rootComponent).when(treeRootHolder).getRoot(); - - MeasureRepository measureRepository = mock(MeasureRepository.class); - doReturn(Optional.of(Measure.newMeasureBuilder().create(12.3, 2, "data"))).when(measureRepository) - .getRawMeasure(eq(rootComponent), any(Metric.class)); - doReturn(measureRepository).when(measuresHolder).getMeasureRepository(); - - MetricRepository metricRepository = mock(MetricRepository.class); - doReturn(mock(Metric.class)).when(metricRepository).getByKey(anyString()); - doReturn(metricRepository).when(measuresHolder).getMetricRepository(); - - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); + when(project.getName()).thenReturn("Project Name"); + when(projectAnalysis.getProject()).thenReturn(project); AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000", scannerContext); - - Formatter formatter = mock(Formatter.class); - doReturn("formatted content").when(formatter).format(any(), any()); - FormatterFactory formatterFactory = mock(FormatterFactory.class); - doReturn(formatter).when(formatterFactory).documentFormatter(); - - assertEquals("formatted content", testCase.createAnalysisSummary(formatterFactory)); - - ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); - verify(formatter).format(documentArgumentCaptor.capture(), eq(formatterFactory)); - - Document expectedDocument = new Document(new Paragraph(new Image("Failed", - "http://localhost:9000/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true")), - new List(List.Style.BULLET, - new ListItem(new Text("12 Lines to Cover (is less than 20)")), - new ListItem(new Text("2 Code Smells (is greater than 0)")), - new ListItem(new Text( - "68.00% Line Coverage (is less than 80.00%)")), - new ListItem(new Text( - "E Security Rating on New Code (is worse than D)")), - new ListItem( - new Text("A Reliability Rating (is better than C)")), - new ListItem(new Text( - "16.00% Condition Coverage (is greater than 15.00%)"))), - new Heading(1, new Text("Analysis Details")), - new Heading(2, new Text("5 Issues")), new List(List.Style.BULLET, - new ListItem( - new Image("Bug", - "http://localhost:9000/static/communityBranchPlugin/common/bug.svg?sanitize=true"), - new Text(" "), - new Text( - "2 Bugs")), - new ListItem(new Image( - "Vulnerability", - "http://localhost:9000/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true"), - new Text( - " "), - new Text( - "2 Vulnerabilities")), - new ListItem(new Image( - "Code Smell", - "http://localhost:9000/static/communityBranchPlugin/common/code_smell.svg?sanitize=true"), - new Text( - " "), - new Text( - "1 Code Smell"))), - new Heading(2, new Text("Coverage and Duplications")), - new List(List.Style.BULLET, new ListItem( - new Image("No coverage information", - "http://localhost:9000/static/communityBranchPlugin/checks/CoverageChart/NoCoverageInfo.svg?sanitize=true"), - new Text(" "), new Text( - "No coverage information (12.30% Estimated after merge)")), - new ListItem(new Image("No duplication information", - "http://localhost:9000/static/communityBranchPlugin/checks/Duplications/NoDuplicationInfo.svg?sanitize=true"), - new Text(" "), new Text( - "No duplication information (12.30% Estimated after merge)"))), - new Paragraph(new Text("**Project ID:** Project Key")), - new Paragraph(new Link("http://localhost:9000/dashboard?id=Project+Key&pullRequest=5", new Text("View in SonarQube")))); - - assertThat(documentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedDocument); - - } - - - @Test - public void testCreateAnalysisSummary2() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("5").when(branchDetails).getBranchName(); - - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - doReturn(treeRootHolder).when(measuresHolder).getTreeRootHolder(); + new AnalysisDetails("pullRequestKey", "commitHash", new ArrayList<>(), qualityGate, projectAnalysis); + + assertEquals("Project Name", testCase.getAnalysisProjectName()); + } + + @Test + void shouldOnlyReturnNonClosedFileIssuesWithScmInfo() { + PostAnalysisIssueVisitor.LightIssue lightIssue1 = mock(PostAnalysisIssueVisitor.LightIssue.class); + when(lightIssue1.status()).thenReturn(Issue.STATUS_OPEN); + Component component1 = mock(Component.class); + when(component1.getType()).thenReturn(Component.Type.FILE); + ReportAttributes reportAttributes1 = mock(ReportAttributes.class); + when(reportAttributes1.getScmPath()).thenReturn(Optional.of("path")); + when(component1.getReportAttributes()).thenReturn(reportAttributes1); + PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = new PostAnalysisIssueVisitor.ComponentIssue(component1, lightIssue1); + + PostAnalysisIssueVisitor.LightIssue lightIssue2 = mock(PostAnalysisIssueVisitor.LightIssue.class); + when(lightIssue2.status()).thenReturn(Issue.STATUS_OPEN); + Component component2 = mock(Component.class); + when(component2.getType()).thenReturn(Component.Type.FILE); + ReportAttributes reportAttributes2 = mock(ReportAttributes.class); + when(reportAttributes2.getScmPath()).thenReturn(Optional.empty()); + when(component2.getReportAttributes()).thenReturn(reportAttributes2); + PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = new PostAnalysisIssueVisitor.ComponentIssue(component2, lightIssue2); + + PostAnalysisIssueVisitor.LightIssue lightIssue3 = mock(PostAnalysisIssueVisitor.LightIssue.class); + when(lightIssue3.status()).thenReturn(Issue.STATUS_OPEN); + Component component3 = mock(Component.class); + when(component3.getType()).thenReturn(Component.Type.PROJECT); + ReportAttributes reportAttributes3 = mock(ReportAttributes.class); + when(reportAttributes3.getScmPath()).thenReturn(Optional.of("path")); + when(component3.getReportAttributes()).thenReturn(reportAttributes3); + PostAnalysisIssueVisitor.ComponentIssue componentIssue3 = new PostAnalysisIssueVisitor.ComponentIssue(component3, lightIssue3); + + PostAnalysisIssueVisitor.LightIssue lightIssue4 = mock(PostAnalysisIssueVisitor.LightIssue.class); + when(lightIssue4.status()).thenReturn(Issue.STATUS_CLOSED); + Component component4 = mock(Component.class); + when(component4.getType()).thenReturn(Component.Type.FILE); + ReportAttributes reportAttributes4 = mock(ReportAttributes.class); + when(reportAttributes4.getScmPath()).thenReturn(Optional.of("path")); + when(component4.getReportAttributes()).thenReturn(reportAttributes4); + PostAnalysisIssueVisitor.ComponentIssue componentIssue4 = new PostAnalysisIssueVisitor.ComponentIssue(component4, lightIssue4); PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - doReturn(new ArrayList<>()).when(postAnalysisIssueVisitor).getIssues(); - - QualityGate.Condition duplicationsCondition = mock(QualityGate.Condition.class); - doReturn("18").when(duplicationsCondition).getValue(); - doReturn(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY).when(duplicationsCondition).getMetricKey(); - - - QualityGate.Condition coverageCondition = mock(QualityGate.Condition.class); - doReturn("33").when(coverageCondition).getValue(); - doReturn(CoreMetrics.NEW_COVERAGE_KEY).when(coverageCondition).getMetricKey(); - - QualityGate qualityGate = mock(QualityGate.class); - doReturn(QualityGate.Status.OK).when(qualityGate).getStatus(); - doReturn(Arrays.asList(coverageCondition, duplicationsCondition)).when(qualityGate).getConditions(); - - Analysis analysis = mock(Analysis.class); - Project project = mock(Project.class); - doReturn("Project Key").when(project).getKey(); - - Component rootComponent = mock(Component.class); - doReturn(rootComponent).when(treeRootHolder).getRoot(); - - MeasureRepository measureRepository = mock(MeasureRepository.class); - doReturn(Optional.of(Measure.newMeasureBuilder().create(21.782, 2, "data"))).when(measureRepository) - .getRawMeasure(eq(rootComponent), any(Metric.class)); - doReturn(measureRepository).when(measuresHolder).getMeasureRepository(); - - MetricRepository metricRepository = mock(MetricRepository.class); - doReturn(mock(Metric.class)).when(metricRepository).getByKey(anyString()); - doReturn(metricRepository).when(measuresHolder).getMetricRepository(); - - ScannerContext scannerContext = mock(ScannerContext.class); - - Configuration configuration = mock(Configuration.class); - - AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000", scannerContext); - - Formatter formatter = mock(Formatter.class); - doReturn("formatted content").when(formatter).format(any(), any()); - FormatterFactory formatterFactory = mock(FormatterFactory.class); - doReturn(formatter).when(formatterFactory).documentFormatter(); - - assertEquals("formatted content", testCase.createAnalysisSummary(formatterFactory)); - - ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); - verify(formatter).format(documentArgumentCaptor.capture(), eq(formatterFactory)); - - Document expectedDocument = new Document(new Paragraph(new Image("Passed", - "http://localhost:9000/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true")), - new Text(""), new Heading(1, new Text("Analysis Details")), - new Heading(2, new Text("0 Issues")), new List(List.Style.BULLET, - new ListItem( - new Image("Bug", - "http://localhost:9000/static/communityBranchPlugin/common/bug.svg?sanitize=true"), - new Text(" "), - new Text( - "0 Bugs")), - new ListItem(new Image( - "Vulnerability", - "http://localhost:9000/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true"), - new Text( - " "), - new Text( - "0 Vulnerabilities")), - new ListItem(new Image( - "Code Smell", - "http://localhost:9000/static/communityBranchPlugin/common/code_smell.svg?sanitize=true"), - new Text( - " "), - new Text( - "0 Code Smells"))), - new Heading(2, new Text("Coverage and Duplications")), - new List(List.Style.BULLET, new ListItem( - new Image("25 percent coverage", - "http://localhost:9000/static/communityBranchPlugin/checks/CoverageChart/25.svg?sanitize=true"), - new Text(" "), - new Text("33.00% Coverage (21.78% Estimated after merge)")), - new ListItem(new Image("20 percent duplication", - "http://localhost:9000/static/communityBranchPlugin/checks/Duplications/20.svg?sanitize=true"), - new Text(" "), new Text( - "18.00% Duplicated Code (21.78% Estimated after merge)"))), - new Paragraph(new Text("**Project ID:** Project Key")), - new Paragraph(new Link("http://localhost:9000/dashboard?id=Project+Key&pullRequest=5", new Text("View in SonarQube")))); - - assertThat(documentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedDocument); - + when(postAnalysisIssueVisitor.getIssues()).thenReturn(Arrays.asList(componentIssue1, componentIssue2, componentIssue3, componentIssue4)); + + AnalysisDetails underTest = new AnalysisDetails("pullRequest", "commmitId", + Arrays.asList(componentIssue1, componentIssue2, componentIssue3, componentIssue4), + mock(QualityGate.class), mock(PostProjectAnalysisTask.ProjectAnalysis.class)); + + assertThat(underTest.getScmReportableIssues()).containsOnly(componentIssue1); } @Test - public void testCreateAnalysisSummary3() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("5").when(branchDetails).getBranchName(); - - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - doReturn(treeRootHolder).when(measuresHolder).getTreeRootHolder(); - - PostAnalysisIssueVisitor.LightIssue issue = mock(PostAnalysisIssueVisitor.LightIssue.class); - doReturn(Issue.STATUS_OPEN).when(issue).status(); - doReturn(RuleType.BUG).when(issue).type(); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - - PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); - doReturn(issue).when(componentIssue).getIssue(); - doReturn(Collections.singletonList(componentIssue)).when(postAnalysisIssueVisitor).getIssues(); - - QualityGate.Condition duplicationsCondition = mock(QualityGate.Condition.class); - doReturn("10").when(duplicationsCondition).getValue(); - doReturn(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY).when(duplicationsCondition).getMetricKey(); - - - QualityGate.Condition coverageCondition = mock(QualityGate.Condition.class); - doReturn("25").when(coverageCondition).getValue(); - doReturn(CoreMetrics.NEW_COVERAGE_KEY).when(coverageCondition).getMetricKey(); - + void shouldOnlyReturnQualityGateConditionsInErrorState() { QualityGate qualityGate = mock(QualityGate.class); - doReturn(QualityGate.Status.OK).when(qualityGate).getStatus(); - doReturn(Arrays.asList(coverageCondition, duplicationsCondition)).when(qualityGate).getConditions(); - - Analysis analysis = mock(Analysis.class); - Project project = mock(Project.class); - doReturn("Project Key").when(project).getKey(); - - Component rootComponent = mock(Component.class); - doReturn(rootComponent).when(treeRootHolder).getRoot(); - MeasureRepository measureRepository = mock(MeasureRepository.class); - doReturn(Optional.of(Measure.newMeasureBuilder().create(21.782, 2, "data"))).when(measureRepository) - .getRawMeasure(eq(rootComponent), any(Metric.class)); - doReturn(measureRepository).when(measuresHolder).getMeasureRepository(); - - MetricRepository metricRepository = mock(MetricRepository.class); - doReturn(mock(Metric.class)).when(metricRepository).getByKey(anyString()); - doReturn(metricRepository).when(measuresHolder).getMetricRepository(); - - ScannerContext scannerContext = mock(ScannerContext.class); + QualityGate.Condition condition1 = mock(QualityGate.Condition.class); + when(condition1.getStatus()).thenReturn(QualityGate.EvaluationStatus.OK); + QualityGate.Condition condition2 = mock(QualityGate.Condition.class); + when(condition2.getStatus()).thenReturn(QualityGate.EvaluationStatus.ERROR); + QualityGate.Condition condition3 = mock(QualityGate.Condition.class); + when(condition3.getStatus()).thenReturn(QualityGate.EvaluationStatus.NO_VALUE); + QualityGate.Condition condition4 = mock(QualityGate.Condition.class); + when(condition4.getStatus()).thenReturn(QualityGate.EvaluationStatus.WARN); + QualityGate.Condition condition5 = mock(QualityGate.Condition.class); + when(condition5.getStatus()).thenReturn(QualityGate.EvaluationStatus.ERROR); - Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("http://host.name/path")).when(configuration) - .get(eq(CommunityBranchPlugin.IMAGE_URL_BASE)); + when(qualityGate.getConditions()).thenReturn(List.of(condition1, condition2, condition3, condition4, condition5)); - AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000", scannerContext); - - Formatter formatter = mock(Formatter.class); - doReturn("formatted content").when(formatter).format(any(), any()); - FormatterFactory formatterFactory = mock(FormatterFactory.class); - doReturn(formatter).when(formatterFactory).documentFormatter(); - - assertEquals("formatted content", testCase.createAnalysisSummary(formatterFactory)); - - ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); - verify(formatter).format(documentArgumentCaptor.capture(), eq(formatterFactory)); - - Document expectedDocument = new Document(new Paragraph( - new Image("Passed", "http://host.name/path/checks/QualityGateBadge/passed.svg?sanitize=true")), - new Text(""), new Heading(1, new Text("Analysis Details")), - new Heading(2, new Text("1 Issue")), new List(List.Style.BULLET, - new ListItem( - new Image("Bug", - "http://host.name/path/common/bug.svg?sanitize=true"), - new Text(" "), - new Text( - "1 Bug")), - new ListItem(new Image( - "Vulnerability", - "http://host.name/path/common/vulnerability.svg?sanitize=true"), - new Text( - " "), - new Text( - "0 Vulnerabilities")), - new ListItem(new Image( - "Code Smell", - "http://host.name/path/common/code_smell.svg?sanitize=true"), - new Text( - " "), - new Text( - "0 Code Smells"))), - new Heading(2, new Text("Coverage and Duplications")), - new List(List.Style.BULLET, new ListItem( - new Image("25 percent coverage", - "http://host.name/path/checks/CoverageChart/25.svg?sanitize=true"), - new Text(" "), - new Text("25.00% Coverage (21.78% Estimated after merge)")), - new ListItem(new Image("10 percent duplication", - "http://host.name/path/checks/Duplications/10.svg?sanitize=true"), - new Text(" "), new Text( - "10.00% Duplicated Code (21.78% Estimated after merge)"))), - new Paragraph(new Text("**Project ID:** Project Key")), - new Paragraph(new Link("http://localhost:9000/dashboard?id=Project+Key&pullRequest=5", new Text("View in SonarQube")))); - - assertThat(documentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedDocument); + AnalysisDetails underTest = new AnalysisDetails("pullRequest", "commit", List.of(), qualityGate, mock(PostProjectAnalysisTask.ProjectAnalysis.class)); + assertThat(underTest.findFailedQualityGateConditions()).isEqualTo(List.of(condition2, condition5)); } @Test - public void testCreateAnalysisSummary4() { - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("5").when(branchDetails).getBranchName(); - - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - AnalysisDetails.MeasuresHolder measuresHolder = mock(AnalysisDetails.MeasuresHolder.class); - doReturn(treeRootHolder).when(measuresHolder).getTreeRootHolder(); - - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - doReturn(new ArrayList<>()).when(postAnalysisIssueVisitor).getIssues(); - - QualityGate.Condition duplicationsCondition = mock(QualityGate.Condition.class); - doReturn("30").when(duplicationsCondition).getValue(); - doReturn(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY).when(duplicationsCondition).getMetricKey(); - - - QualityGate.Condition coverageCondition = mock(QualityGate.Condition.class); - doReturn("0").when(coverageCondition).getValue(); - doReturn(CoreMetrics.NEW_COVERAGE_KEY).when(coverageCondition).getMetricKey(); - + void shouldFilterOnQualityGateConditionName() { QualityGate qualityGate = mock(QualityGate.class); - doReturn(QualityGate.Status.OK).when(qualityGate).getStatus(); - doReturn(Arrays.asList(coverageCondition, duplicationsCondition)).when(qualityGate).getConditions(); - - Analysis analysis = mock(Analysis.class); - Project project = mock(Project.class); - doReturn("Project Key").when(project).getKey(); - - Component rootComponent = mock(Component.class); - doReturn(rootComponent).when(treeRootHolder).getRoot(); - - MeasureRepository measureRepository = mock(MeasureRepository.class); - doReturn(Optional.of(Measure.newMeasureBuilder().create(21.782, 2, "data"))).when(measureRepository) - .getRawMeasure(eq(rootComponent), any(Metric.class)); - doReturn(measureRepository).when(measuresHolder).getMeasureRepository(); - - MetricRepository metricRepository = mock(MetricRepository.class); - doReturn(mock(Metric.class)).when(metricRepository).getByKey(anyString()); - doReturn(metricRepository).when(measuresHolder).getMetricRepository(); - - ScannerContext scannerContext = mock(ScannerContext.class); - Configuration configuration = mock(Configuration.class); - - AnalysisDetails testCase = - new AnalysisDetails(branchDetails, postAnalysisIssueVisitor, qualityGate, measuresHolder, analysis, - project, configuration, "http://localhost:9000", scannerContext); - - Formatter formatter = mock(Formatter.class); - doReturn("formatted content").when(formatter).format(any(), any()); - FormatterFactory formatterFactory = mock(FormatterFactory.class); - doReturn(formatter).when(formatterFactory).documentFormatter(); - - assertEquals("formatted content", testCase.createAnalysisSummary(formatterFactory)); - - ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); - verify(formatter).format(documentArgumentCaptor.capture(), eq(formatterFactory)); - - Document expectedDocument = new Document(new Paragraph(new Image("Passed", - "http://localhost:9000/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true")), - new Text(""), new Heading(1, new Text("Analysis Details")), - new Heading(2, new Text("0 Issues")), new List(List.Style.BULLET, - new ListItem( - new Image("Bug", - "http://localhost:9000/static/communityBranchPlugin/common/bug.svg?sanitize=true"), - new Text(" "), - new Text( - "0 Bugs")), - new ListItem(new Image( - "Vulnerability", - "http://localhost:9000/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true"), - new Text( - " "), - new Text( - "0 Vulnerabilities")), - new ListItem(new Image( - "Code Smell", - "http://localhost:9000/static/communityBranchPlugin/common/code_smell.svg?sanitize=true"), - new Text( - " "), - new Text( - "0 Code Smells"))), - new Heading(2, new Text("Coverage and Duplications")), - new List(List.Style.BULLET, new ListItem( - new Image("0 percent coverage", - "http://localhost:9000/static/communityBranchPlugin/checks/CoverageChart/0.svg?sanitize=true"), - new Text(" "), - new Text("0.00% Coverage (21.78% Estimated after merge)")), - new ListItem(new Image("20plus percent duplication", - "http://localhost:9000/static/communityBranchPlugin/checks/Duplications/20plus.svg?sanitize=true"), - new Text(" "), new Text( - "30.00% Duplicated Code (21.78% Estimated after merge)"))), - new Paragraph(new Text("**Project ID:** Project Key")), - new Paragraph(new Link("http://localhost:9000/dashboard?id=Project+Key&pullRequest=5", new Text("View in SonarQube")))); - - assertThat(documentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedDocument); - - } - - @Test - public void testCorrectMeasuresRepositoryReturned() { - MeasureRepository measureRepository = mock(MeasureRepository.class); - MetricRepository metricRepository = mock(MetricRepository.class); - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - - AnalysisDetails.MeasuresHolder testCase = - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder); - - assertEquals(measureRepository, testCase.getMeasureRepository()); - } - - @Test - public void testCorrectMetricsRepositoryReturned() { - MeasureRepository measureRepository = mock(MeasureRepository.class); - MetricRepository metricRepository = mock(MetricRepository.class); - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - - AnalysisDetails.MeasuresHolder testCase = - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder); - - assertEquals(metricRepository, testCase.getMetricRepository()); - } - - @Test - public void testCorrectTreeRootHolderReturned() { - MeasureRepository measureRepository = mock(MeasureRepository.class); - MetricRepository metricRepository = mock(MetricRepository.class); - TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - AnalysisDetails.MeasuresHolder testCase = - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, treeRootHolder); + List conditions = IntStream.range(0, 10).mapToObj(i -> { + QualityGate.Condition condition = mock(QualityGate.Condition.class); + when(condition.getMetricKey()).thenReturn("key" + i); + return condition; + }).collect(Collectors.toList()); - assertEquals(treeRootHolder, testCase.getTreeRootHolder()); - } - - @Test - public void testCorrectPostAnalysisIssueVisitorReturned() { - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - AnalysisDetails analysisDetails = - new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), postAnalysisIssueVisitor, - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), mock(Project.class), mock(Configuration.class), null, - mock(ScannerContext.class)); - assertSame(postAnalysisIssueVisitor, analysisDetails.getPostAnalysisIssueVisitor()); - } - - @Test - public void testCorrectBranchDetailsReturned() { - AnalysisDetails.BranchDetails branchDetails = new AnalysisDetails.BranchDetails("branchName", "commitId"); - assertEquals("branchName", branchDetails.getBranchName()); - assertEquals("commitId", branchDetails.getCommitId()); - } - - @Test - public void testGetBaseImageUrlFromConfig() { - Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("http://host.name/path")).when(configuration) - .get(eq(CommunityBranchPlugin.IMAGE_URL_BASE)); - - AnalysisDetails analysisDetails = - new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), mock(Project.class), configuration, "http://localhost:9000", mock(ScannerContext.class)); - - assertEquals("http://host.name/path", analysisDetails.getBaseImageUrl()); - } - - @Test - public void testGetBaseImageUrlFromConfigWithTrailingSlash() { - Configuration configuration = mock(Configuration.class); - doReturn(Optional.of("http://host.name/path/")).when(configuration) - .get(eq(CommunityBranchPlugin.IMAGE_URL_BASE)); - - AnalysisDetails analysisDetails = - new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), mock(Project.class), configuration, "http://localhost:9000", mock(ScannerContext.class)); - - assertEquals("http://host.name/path", analysisDetails.getBaseImageUrl()); - } - - @Test - public void testGetBaseImageUrlFromRootUrl() { - AnalysisDetails analysisDetails = - new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), mock(Project.class), mock(Configuration.class), "http://localhost:9000", mock(ScannerContext.class)); - - assertEquals("http://localhost:9000/static/communityBranchPlugin", analysisDetails.getBaseImageUrl()); - } - - @Test - public void testGetIssueUrlBug() { - Project project = mock(Project.class); - doReturn("projectKey").when(project).getKey(); - - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("123").when(branchDetails).getBranchName(); - - AnalysisDetails analysisDetails = - new AnalysisDetails(branchDetails, mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), project, mock(Configuration.class), "http://localhost:9000", mock(ScannerContext.class)); - - PostAnalysisIssueVisitor.LightIssue lightIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(lightIssue.key()).thenReturn("issueKey"); - when(lightIssue.type()).thenReturn(RuleType.BUG); - - assertEquals("http://localhost:9000/project/issues?id=projectKey&pullRequest=123&issues=issueKey&open=issueKey", analysisDetails.getIssueUrl(lightIssue)); - } - - @Test - public void testGetIssueUrlSecurityHotspot() { - Project project = mock(Project.class); - doReturn("projectKey").when(project).getKey(); - - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - doReturn("123").when(branchDetails).getBranchName(); - - AnalysisDetails analysisDetails = - new AnalysisDetails(branchDetails, mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), project, mock(Configuration.class), "http://localhost:9000", mock(ScannerContext.class)); + when(qualityGate.getConditions()).thenReturn(conditions); - PostAnalysisIssueVisitor.LightIssue lightIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(lightIssue.key()).thenReturn("secondIssueKey"); - when(lightIssue.type()).thenReturn(RuleType.SECURITY_HOTSPOT); + AnalysisDetails underTest = new AnalysisDetails("pullRequest", "commit", List.of(), qualityGate, mock(PostProjectAnalysisTask.ProjectAnalysis.class)); - assertEquals("http://localhost:9000/security_hotspots?id=projectKey&pullRequest=123&hotspots=secondIssueKey", analysisDetails.getIssueUrl(lightIssue)); + assertThat(underTest.findQualityGateCondition("key2")).contains(conditions.get(2)); } @Test - public void testGetRuleUrlWithRuleKey() { - AnalysisDetails analysisDetails = - new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), mock(Project.class), mock(Configuration.class), "http://localhost:9000", mock(ScannerContext.class)); + void shouldRetrievePropertyFromScannerProperties() { + Map scannerProperties = mock(Map.class); + when(scannerProperties.get(anyString())).thenReturn("world"); - assertEquals("http://localhost:9000/coding_rules?open=ruleKey&rule_key=ruleKey", analysisDetails.getRuleUrlWithRuleKey("ruleKey")); - } - - @Test - public void testCreateAnalysisIssueSummary() { - FormatterFactory formatterFactory = mock(FormatterFactory.class); - PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + ScannerContext scannerContext = mock(ScannerContext.class); + when(scannerContext.getProperties()).thenReturn(scannerProperties); + PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); + when(projectAnalysis.getScannerContext()).thenReturn(scannerContext); - AnalysisDetails.BranchDetails branchDetails = mock(AnalysisDetails.BranchDetails.class); - when(branchDetails.getBranchName()).thenReturn("branchName"); + AnalysisDetails underTest = new AnalysisDetails("PullRequest", "Commit", List.of(), mock(QualityGate.class), projectAnalysis); - PostAnalysisIssueVisitor.LightIssue lightIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); - when(lightIssue.type()).thenReturn(RuleType.BUG); - when(lightIssue.getMessage()).thenReturn("message"); - when(lightIssue.severity()).thenReturn("severity"); - when(lightIssue.key()).thenReturn("issueKey"); - when(lightIssue.effortInMinutes()).thenReturn(123L); - when(componentIssue.getIssue()).thenReturn(lightIssue); + assertThat(underTest.getScannerProperty("hello")).contains("world"); - Project project = mock(Project.class); - when(project.getKey()).thenReturn("projectKey"); - - Formatter documentFormatter = mock(Formatter.class); - when(formatterFactory.documentFormatter()).thenReturn(documentFormatter); - - AnalysisDetails analysisDetails = - new AnalysisDetails(branchDetails, mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), - mock(Analysis.class), project, mock(Configuration.class), "http://localhost:9000", mock(ScannerContext.class)); - - ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); - analysisDetails.createAnalysisIssueSummary(componentIssue, formatterFactory); - verify(documentFormatter).format(documentArgumentCaptor.capture(), any()); - - assertThat(documentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo( - new Document( - new Paragraph( - new Text("**Type:** BUG "), - new Image("BUG", "http://localhost:9000/static/communityBranchPlugin/checks/IssueType/bug.svg?sanitize=true") - ), - new Paragraph( - new Text("**Severity:** severity "), - new Image("severity", "http://localhost:9000/static/communityBranchPlugin/checks/Severity/severity.svg?sanitize=true") - ), - new Paragraph(new Text("**Message:** message")), - new Paragraph(new Text("**Duration (min):** 123")), - new Text(""), - new Paragraph(new Text("**Project ID:** projectKey **Issue ID:** issueKey")), - new Paragraph(new Link("http://localhost:9000/project/issues?id=projectKey&pullRequest=branchName&issues=issueKey&open=issueKey", new Text("View in SonarQube"))) - ) - ); + verify(scannerProperties).get("hello"); } @Test - public void testFakeIdReturnedForSummaryComment() { - AnalysisDetails analysisDetails = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), mock(Analysis.class), mock(Project.class), - mock(Configuration.class),"", mock(ScannerContext.class)); - assertThat(analysisDetails.parseIssueIdFromUrl("https://sonarqube.dummy/path/dashboard?id=project&pullRequest=123")) - .get() - .usingRecursiveComparison() - .isEqualTo(new AnalysisDetails.ProjectIssueIdentifier("project", "decorator-summary-comment")); - } + void shouldReturnPullRequestId() { + AnalysisDetails underTest = new AnalysisDetails("pull-request-id", "commit-id", List.of(), mock(QualityGate.class), mock(PostProjectAnalysisTask.ProjectAnalysis.class)); - @Test - public void testIssueIdReturnedForHotspotUrl() { - AnalysisDetails analysisDetails = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), mock(Analysis.class), mock(Project.class), - mock(Configuration.class),"", mock(ScannerContext.class)); - assertThat(analysisDetails.parseIssueIdFromUrl("http://subdomain.sonarqube.dummy/path/security_hotspots?id=projectIdentifier&hotspots=A1B2-Z9Y8X7")) - .get() - .usingRecursiveComparison() - .isEqualTo(new AnalysisDetails.ProjectIssueIdentifier("projectIdentifier", "A1B2-Z9Y8X7")); + assertThat(underTest.getPullRequestId()).isEqualTo("pull-request-id"); } - @Test - public void testNoIssueIdReturnedForHotspotUrlWithoutId() { - AnalysisDetails analysisDetails = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), mock(Analysis.class), mock(Project.class), - mock(Configuration.class),"", mock(ScannerContext.class)); - assertThat(analysisDetails.parseIssueIdFromUrl("http://subdomain.sonarqube.dummy/path/security_hotspots?id=projectId&other_parameter=ABC")) - .isEmpty(); - } @Test - public void testIssueIdReturnedForIssueUrl() { - AnalysisDetails analysisDetails = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), mock(Analysis.class), mock(Project.class), - mock(Configuration.class),"", mock(ScannerContext.class)); - assertThat(analysisDetails.parseIssueIdFromUrl("http://subdomain.sonarqube.dummy/path/issue?id=projectId&issues=XXX-YYY-ZZZ")) - .get() - .usingRecursiveComparison() - .isEqualTo(new AnalysisDetails.ProjectIssueIdentifier("projectId", "XXX-YYY-ZZZ")); - } + void shouldReturnCommitSha() { + AnalysisDetails underTest = new AnalysisDetails("pull-request-id", "commit-id", List.of(), mock(QualityGate.class), mock(PostProjectAnalysisTask.ProjectAnalysis.class)); - @Test - public void testNoIssueIdReturnedForIssueUrlWithoutId() { - AnalysisDetails analysisDetails = new AnalysisDetails(mock(AnalysisDetails.BranchDetails.class), mock(PostAnalysisIssueVisitor.class), - mock(QualityGate.class), mock(AnalysisDetails.MeasuresHolder.class), mock(Analysis.class), mock(Project.class), - mock(Configuration.class),"", mock(ScannerContext.class)); - assertThat(analysisDetails.parseIssueIdFromUrl("http://subdomain.sonarqube.dummy/path/issue?id=projectId&other_parameter=123")).isEmpty(); + assertThat(underTest.getCommitSha()).isEqualTo("commit-id"); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitorTest.java index f8672cbdc..7568e5158 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PostAnalysisIssueVisitorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -22,11 +22,13 @@ import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.ReportAttributes; import org.sonar.core.issue.DefaultIssue; import org.sonar.db.protobuf.DbIssues; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -35,6 +37,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; public class PostAnalysisIssueVisitorTest { @@ -55,9 +58,9 @@ public void checkAllIssuesCollected() { List expected = new ArrayList<>(); for (int i = 0; i < 100; i++) { - DefaultIssue issue = (i == 10 ? null : mock(DefaultIssue.class)); - Component component = (i == 5 ? null : mock(Component.class)); - expected.add(new PostAnalysisIssueVisitor.ComponentIssue(component, issue)); + DefaultIssue issue = mock(DefaultIssue.class); + Component component = mock(Component.class); + expected.add(new PostAnalysisIssueVisitor.ComponentIssue(component, new PostAnalysisIssueVisitor.LightIssue(issue))); testCase.onIssue(component, issue); } @@ -89,10 +92,11 @@ private DefaultIssue exampleDefaultIssue() { public void testLightIssueMapping() { // mock a DefaultIssue DefaultIssue defaultIssue = exampleDefaultIssue(); + Component component = mock(Component.class); // map the DefaultIssue into a LightIssue (using PostAnalysisIssueVisitor to workaround private constructor) PostAnalysisIssueVisitor visitor = new PostAnalysisIssueVisitor(); - visitor.onIssue(null, defaultIssue); + visitor.onIssue(component, defaultIssue); PostAnalysisIssueVisitor.LightIssue lightIssue = visitor.getIssues().get(0).getIssue(); // check values equality, twice (see below) @@ -127,11 +131,12 @@ public void testLightIssueMapping() { @Test public void testEqualLightIssues() { DefaultIssue defaultIssue = exampleDefaultIssue(); + Component component = mock(Component.class); // map the DefaultIssue into two equal LightIssues PostAnalysisIssueVisitor visitor = new PostAnalysisIssueVisitor(); - visitor.onIssue(null, defaultIssue); - visitor.onIssue(null, defaultIssue); + visitor.onIssue(component, defaultIssue); + visitor.onIssue(component, defaultIssue); PostAnalysisIssueVisitor.LightIssue lightIssue1 = visitor.getIssues().get(0).getIssue(); PostAnalysisIssueVisitor.LightIssue lightIssue2 = visitor.getIssues().get(1).getIssue(); @@ -147,15 +152,16 @@ public void testEqualLightIssues() { @Test public void testDifferentLightIssues() { DefaultIssue defaultIssue = exampleDefaultIssue(); + Component component = mock(Component.class); // map the DefaultIssue into a first LightIssue PostAnalysisIssueVisitor visitor = new PostAnalysisIssueVisitor(); - visitor.onIssue(null, defaultIssue); + visitor.onIssue(component, defaultIssue); PostAnalysisIssueVisitor.LightIssue lightIssue1 = visitor.getIssues().get(0).getIssue(); - // map a slightly different DefaultIssue into an other LightIssue + // map a slightly different DefaultIssue into another LightIssue doReturn("another message").when(defaultIssue).getMessage(); - visitor.onIssue(null, defaultIssue); + visitor.onIssue(component, defaultIssue); PostAnalysisIssueVisitor.LightIssue lightIssue2 = visitor.getIssues().get(1).getIssue(); // assert difference @@ -168,4 +174,47 @@ public void testDifferentLightIssues() { } + @Test + public void shouldReturnScmInfoForFileComponent() { + Component component = mock(Component.class); + when(component.getType()).thenReturn(Component.Type.FILE); + ReportAttributes reportAttributes = mock(ReportAttributes.class); + when(reportAttributes.getScmPath()).thenReturn(Optional.of("path")); + when(component.getReportAttributes()).thenReturn(reportAttributes); + + PostAnalysisIssueVisitor.LightIssue issue = mock(PostAnalysisIssueVisitor.LightIssue.class); + PostAnalysisIssueVisitor.ComponentIssue underTest = new PostAnalysisIssueVisitor.ComponentIssue(component, issue); + + assertThat(underTest.getScmPath()).contains("path"); + } + + @Test + public void shouldReturnNoScmInfoForNonFileComponent() { + Component component = mock(Component.class); + when(component.getType()).thenReturn(Component.Type.PROJECT); + ReportAttributes reportAttributes = mock(ReportAttributes.class); + when(reportAttributes.getScmPath()).thenReturn(Optional.of("path")); + when(component.getReportAttributes()).thenReturn(reportAttributes); + + PostAnalysisIssueVisitor.LightIssue issue = mock(PostAnalysisIssueVisitor.LightIssue.class); + PostAnalysisIssueVisitor.ComponentIssue underTest = new PostAnalysisIssueVisitor.ComponentIssue(component, issue); + + assertThat(underTest.getScmPath()).isEmpty(); + } + + @Test + public void shouldReturnNoScmInfoForFileComponentWithNoInfo() { + Component component = mock(Component.class); + when(component.getType()).thenReturn(Component.Type.FILE); + ReportAttributes reportAttributes = mock(ReportAttributes.class); + when(reportAttributes.getScmPath()).thenReturn(Optional.empty()); + when(component.getReportAttributes()).thenReturn(reportAttributes); + + PostAnalysisIssueVisitor.LightIssue issue = mock(PostAnalysisIssueVisitor.LightIssue.class); + PostAnalysisIssueVisitor.ComponentIssue underTest = new PostAnalysisIssueVisitor.ComponentIssue(component, issue); + + assertThat(underTest.getScmPath()).isEmpty(); + } + + } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java index f4da3d69a..e1bc20be6 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,8 +18,8 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.ce.posttask.Analysis; import org.sonar.api.ce.posttask.Branch; @@ -27,11 +27,6 @@ import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.ce.posttask.ScannerContext; -import org.sonar.api.config.Configuration; -import org.sonar.api.platform.Server; -import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; -import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; -import org.sonar.ce.task.projectanalysis.metric.MetricRepository; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.alm.setting.ALM; @@ -62,42 +57,36 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class PullRequestPostAnalysisTaskTest { - - private PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); - private Branch branch = mock(Branch.class); - private ScannerContext scannerContext = mock(ScannerContext.class); - - private Server server = mock(Server.class); - private List pullRequestBuildStatusDecorators = new ArrayList<>(); - private PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - private MetricRepository metricRepository = mock(MetricRepository.class); - private MeasureRepository measureRepository = mock(MeasureRepository.class); - private TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); - private PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); - private DbClient dbClient = mock(DbClient.class); - private Project project = mock(Project.class); - private Configuration configuration = mock(Configuration.class); - - private PullRequestPostAnalysisTask testCase = - new PullRequestPostAnalysisTask(server, pullRequestBuildStatusDecorators, - postAnalysisIssueVisitor, metricRepository, measureRepository, - treeRootHolder, configuration, dbClient); - - @Before - public void init() { +class PullRequestPostAnalysisTaskTest { + + private final PostProjectAnalysisTask.ProjectAnalysis projectAnalysis = mock(PostProjectAnalysisTask.ProjectAnalysis.class); + private final Branch branch = mock(Branch.class); + private final ScannerContext scannerContext = mock(ScannerContext.class); + + private final List pullRequestBuildStatusDecorators = new ArrayList<>(); + private final PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); + private final PostProjectAnalysisTask.Context context = mock(PostProjectAnalysisTask.Context.class); + private final DbClient dbClient = mock(DbClient.class); + private final Project project = mock(Project.class); + private final List componentIssues = List.of(mock(PostAnalysisIssueVisitor.ComponentIssue.class)); + + private final PullRequestPostAnalysisTask testCase = + new PullRequestPostAnalysisTask(pullRequestBuildStatusDecorators, + postAnalysisIssueVisitor, dbClient); + + @BeforeEach + void init() { doReturn(Optional.of(branch)).when(projectAnalysis).getBranch(); doReturn(scannerContext).when(projectAnalysis).getScannerContext(); doReturn(new HashMap<>()).when(scannerContext).getProperties(); doReturn(projectAnalysis).when(context).getProjectAnalysis(); doReturn(project).when(projectAnalysis).getProject(); doReturn("uuid").when(project).getUuid(); - - + doReturn(componentIssues).when(postAnalysisIssueVisitor).getIssues(); } @Test - public void testFinishedNonPullRequest() { + void testFinishedNonPullRequest() { doReturn(Branch.Type.BRANCH).when(branch).getType(); testCase.finished(context); @@ -107,7 +96,7 @@ public void testFinishedNonPullRequest() { } @Test - public void testFinishedNoBranchName() { + void testFinishedNoBranchName() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.empty()).when(branch).getName(); @@ -117,7 +106,7 @@ public void testFinishedNoBranchName() { } @Test - public void testFinishedNoProviderSet() { + void testFinishedNoProviderSet() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("branchName")).when(branch).getName(); @@ -145,7 +134,7 @@ public void testFinishedNoProviderSet() { } @Test - public void testFinishedNoProviderMatchingName() { + void testFinishedNoProviderMatchingName() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("branchName")).when(branch).getName(); @@ -181,7 +170,7 @@ public void testFinishedNoProviderMatchingName() { } @Test - public void testFinishedNoAnalysis() { + void testFinishedNoAnalysis() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("pull-request")).when(branch).getName(); @@ -219,7 +208,7 @@ public void testFinishedNoAnalysis() { @Test - public void testFinishedAnalysisWithNoRevision() { + void testFinishedAnalysisWithNoRevision() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("pull-request")).when(branch).getName(); @@ -259,7 +248,7 @@ public void testFinishedAnalysisWithNoRevision() { } @Test - public void testFinishedAnalysisWithNoQualityGate() { + void testFinishedAnalysisWithNoQualityGate() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("pull-request")).when(branch).getName(); @@ -302,7 +291,7 @@ public void testFinishedAnalysisWithNoQualityGate() { } @Test - public void testFinishedAnalysisDecorationRequest() { + void testFinishedAnalysisDecorationRequest() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("pull-request")).when(branch).getName(); @@ -357,16 +346,12 @@ public void testFinishedAnalysisDecorationRequest() { verify(decorator2).decorateQualityGateStatus(analysisDetailsArgumentCaptor.capture(), eq(almSettingDto), eq(projectAlmSettingDto)); AnalysisDetails analysisDetails = - new AnalysisDetails(new AnalysisDetails.BranchDetails("pull-request", "revision"), - postAnalysisIssueVisitor, qualityGate, - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, - treeRootHolder), analysis, project, - configuration ,null, scannerContext); + new AnalysisDetails("pull-request", "revision", componentIssues, qualityGate, projectAnalysis); assertThat(analysisDetailsArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(analysisDetails); } @Test - public void testFinishedAnalysisDecorationRequestPullRequestLinkSaved() { + void testFinishedAnalysisDecorationRequestPullRequestLinkSaved() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("pull-request")).when(branch).getName(); @@ -404,10 +389,10 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkSaved() { doReturn(DbProjectBranches.PullRequestData.newBuilder().build()).when(branchDto).getPullRequestData(); ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); - doReturn(Optional.of(projectAlmSettingDto)).when(projectAlmSettingDao).selectByProject(eq(dbSession), eq("uuid")); + doReturn(Optional.of(projectAlmSettingDto)).when(projectAlmSettingDao).selectByProject(dbSession, "uuid"); doReturn("setting-uuid").when(projectAlmSettingDto).getAlmSettingUuid(); AlmSettingDao almSettingDao = mock(AlmSettingDao.class); - doReturn(Optional.of(almSettingDto)).when(almSettingDao).selectByUuid(eq(dbSession), eq("setting-uuid")); + doReturn(Optional.of(almSettingDto)).when(almSettingDao).selectByUuid(dbSession, "setting-uuid"); doReturn(projectAlmSettingDao).when(dbClient).projectAlmSettingDao(); doReturn(almSettingDao).when(dbClient).almSettingDao(); @@ -417,9 +402,9 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkSaved() { ArgumentCaptor analysisDetailsArgumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); verify(projectAnalysis).getAnalysis(); verify(projectAnalysis).getQualityGate(); - verify(dbClient, times(2)).openSession(eq(false)); + verify(dbClient, times(2)).openSession(false); verify(dbClient).branchDao(); - verify(branchDao).selectByPullRequestKey(eq(dbSession), eq("uuid"), eq("pull-request")); + verify(branchDao).selectByPullRequestKey(dbSession, "uuid", "pull-request"); verify(decorator2).decorateQualityGateStatus(analysisDetailsArgumentCaptor.capture(), eq(almSettingDto), eq(projectAlmSettingDto)); ArgumentCaptor pullRequestDataArgumentCaptor = ArgumentCaptor.forClass( @@ -428,19 +413,16 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkSaved() { assertThat(pullRequestDataArgumentCaptor.getValue().getUrl()).isEqualTo("pullRequestUrl"); verify(dbSession).commit(); - verify(branchDao).upsert(eq(dbSession), eq(branchDto)); + verify(branchDao).upsert(dbSession, branchDto); AnalysisDetails analysisDetails = - new AnalysisDetails(new AnalysisDetails.BranchDetails("pull-request", "revision"), - postAnalysisIssueVisitor, qualityGate, - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, - treeRootHolder), analysis, project, - configuration ,null, scannerContext); + new AnalysisDetails("pull-request", "revision", + componentIssues, qualityGate, projectAnalysis); assertThat(analysisDetailsArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(analysisDetails); } @Test - public void testFinishedAnalysisDecorationRequestPullRequestLinkNotSavedIfBranchDtoMissing() { + void testFinishedAnalysisDecorationRequestPullRequestLinkNotSavedIfBranchDtoMissing() { doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); doReturn(Optional.of("pull-request")).when(branch).getName(); @@ -497,9 +479,9 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkNotSavedIfBranch verify(projectAnalysis).getAnalysis(); verify(projectAnalysis).getQualityGate(); - verify(dbClient, times(2)).openSession(eq(false)); + verify(dbClient, times(2)).openSession(false); verify(dbClient).branchDao(); - verify(branchDao).selectByPullRequestKey(eq(dbSession), eq("uuid"), eq("pull-request")); + verify(branchDao).selectByPullRequestKey(dbSession, "uuid", "pull-request"); verify(decorator1).decorateQualityGateStatus(analysisDetailsArgumentCaptor.capture(), almSettingDtoArgumentCaptor.capture(), projectAlmSettingDtoArgumentCaptor.capture()); @@ -511,16 +493,13 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkNotSavedIfBranch verify(branchDao, never()).upsert(any(), any()); AnalysisDetails analysisDetails = - new AnalysisDetails(new AnalysisDetails.BranchDetails("pull-request", "revision"), - postAnalysisIssueVisitor, qualityGate, - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, - treeRootHolder), analysis, project, - configuration ,null, scannerContext); + new AnalysisDetails("pull-request", "revision", + componentIssues, qualityGate, projectAnalysis); assertThat(analysisDetailsArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(analysisDetails); } @Test - public void testCorrectDescriptionReturnedForTask() { + void testCorrectDescriptionReturnedForTask() { assertThat(testCase.getDescription()).isEqualTo("Pull Request Decoration"); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java index c1f30863b..0414bacc6 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/azuredevops/AzureDevOpsPullRequestDecoratorTest.java @@ -2,9 +2,15 @@ import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.AzureDevopsClientFactory; import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.DefaultAzureDevopsClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.PullRequest; +import com.github.mc1arke.sonarqube.plugin.almclient.azuredevops.model.Repository; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Before; import org.junit.Rule; @@ -13,7 +19,6 @@ import org.sonar.api.config.internal.Encryption; import org.sonar.api.config.internal.Settings; import org.sonar.api.issue.Issue; -import org.sonar.api.platform.Server; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.ce.task.projectanalysis.component.Component; @@ -26,6 +31,7 @@ import org.sonar.db.protobuf.DbIssues; import java.util.Collections; +import java.util.List; import java.util.Optional; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -47,7 +53,7 @@ public class AzureDevOpsPullRequestDecoratorTest { @Rule - public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig()); + public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort()); private final String azureProject = "azure Project"; private final String sonarProject = "sonarProject"; @@ -59,8 +65,6 @@ public class AzureDevOpsPullRequestDecoratorTest { private final String issueKeyVal = "issueKeyVal"; private final String ruleKeyVal = "ruleKeyVal"; private final String threadId = "1468"; - private final String issueUrl = "http://sonar:9000/sonar/project/issues?id=sonarProject&pullRequest=8513&issues=issueKeyVal&open=issueKeyVal"; - private final String ruleUrl = "http://sonar:9000/sonar/coding_rules?open=ruleKeyVal&rule_key=ruleKeyVal"; private final int lineNumber = 5; private final String token = "token"; private final String authHeader = "Basic OnRva2Vu"; @@ -69,14 +73,14 @@ public class AzureDevOpsPullRequestDecoratorTest { private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - private final Server server = mock(Server.class); private final ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); private final Settings settings = mock(Settings.class); private final Encryption encryption = mock(Encryption.class); - private final AzureDevOpsPullRequestDecorator pullRequestDecorator = new AzureDevOpsPullRequestDecorator(server, scmInfoRepository, new DefaultAzureDevopsClientFactory(settings)); + private final ReportGenerator reportGenerator = mock(ReportGenerator.class); + private final MarkdownFormatterFactory formatterFactory = mock(MarkdownFormatterFactory.class); + private final AzureDevOpsPullRequestDecorator pullRequestDecorator = new AzureDevOpsPullRequestDecorator(scmInfoRepository, new DefaultAzureDevopsClientFactory(settings), reportGenerator, formatterFactory); private final AnalysisDetails analysisDetails = mock(AnalysisDetails.class); - private final PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); private final PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); private final PostAnalysisIssueVisitor.LightIssue defaultIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); private final Component component = mock(Component.class); @@ -84,6 +88,8 @@ public class AzureDevOpsPullRequestDecoratorTest { @Before public void setUp() { when(settings.getEncryption()).thenReturn(encryption); + when(reportGenerator.createAnalysisIssueSummary(any(), any())).thenReturn(mock(AnalysisIssueSummary.class)); + when(reportGenerator.createAnalysisSummary(any())).thenReturn(mock(AnalysisSummary.class)); } private void configureTestDefaults() { @@ -93,22 +99,23 @@ private void configureTestDefaults() { when(analysisDetails.getAnalysisProjectName()).thenReturn(projectName); when(analysisDetails.getAnalysisProjectKey()).thenReturn(sonarProject); when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); - when(analysisDetails.getBranchName()).thenReturn(pullRequestId); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); - when(analysisDetails.getRuleUrlWithRuleKey(ruleKeyVal)).thenReturn(ruleUrl); - when(analysisDetails.getIssueUrl(defaultIssue)).thenReturn(issueUrl); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); - when(analysisDetails.parseIssueIdFromUrl(any())).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier("projectKey", "issueid"))); - when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + when(analysisDetails.getPullRequestId()).thenReturn(pullRequestId); + when(analysisDetails.getIssues()).thenReturn(List.of(componentIssue)); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("analysis summary"); - when(analysisDetails.createAnalysisIssueSummary(any(), any())).thenReturn("issue summary"); + AnalysisSummary analysisSummary = mock(AnalysisSummary.class); + when(analysisSummary.format(any())).thenReturn("analysis summary"); + when(analysisSummary.getDashboardUrl()).thenReturn("http://sonar:9000/sonar/dashboard?id=" + sonarProject + "&pullRequest=" + pullRequestId); + AnalysisIssueSummary analysisIssueSummary = mock(AnalysisIssueSummary.class); + + when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary); + when(reportGenerator.createAnalysisIssueSummary(any(), any())).thenReturn(analysisIssueSummary); DbIssues.Locations locate = DbIssues.Locations.newBuilder().build(); RuleType rule = RuleType.CODE_SMELL; RuleKey ruleKey = mock(RuleKey.class); when(componentIssue.getIssue()).thenReturn(defaultIssue); when(componentIssue.getComponent()).thenReturn(component); + when(componentIssue.getScmPath()).thenReturn(Optional.of("scmPath")); when(defaultIssue.getStatus()).thenReturn(Issue.STATUS_OPEN); when(defaultIssue.getLine()).thenReturn(lineNumber); when(defaultIssue.getLocations()).thenReturn(locate); @@ -116,7 +123,6 @@ private void configureTestDefaults() { when(defaultIssue.getMessage()).thenReturn(issueMessage); when(defaultIssue.getRuleKey()).thenReturn(ruleKey); when(defaultIssue.key()).thenReturn(issueKeyVal); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of("scmPath")); Changeset changeset = mock(Changeset.class); when(changeset.getRevision()).thenReturn("revisionId"); ScmInfo scmInfo = mock(ScmInfo.class); @@ -124,7 +130,6 @@ private void configureTestDefaults() { when(scmInfo.getChangesetForLine(anyInt())).thenReturn(changeset); when(scmInfoRepository.getScmInfo(component)).thenReturn(Optional.of(scmInfo)); when(ruleKey.toString()).thenReturn(ruleKeyVal); - when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); when(projectAlmSettingDto.getAlmSlug()).thenReturn(azureProject); when(projectAlmSettingDto.getAlmRepo()).thenReturn(azureRepository); @@ -226,7 +231,7 @@ private void setupStubs() { " \"state\": \"wellFormed\"," + System.lineSeparator() + " \"revision\": 7" + System.lineSeparator() + " }," + System.lineSeparator() + - " \"remoteUrl\": \"http://localhost:8080/" + azureProject + "/_git/" + azureRepository + "\"" + System.lineSeparator() + + " \"remoteUrl\": \"" + wireMockRule.baseUrl() + "/" + azureProject + "/_git/" + azureRepository + "\"" + System.lineSeparator() + " }," + System.lineSeparator() + " \"pullRequestId\": " + pullRequestId + "," + System.lineSeparator() + " \"codeReviewId\": " + pullRequestId + "," + System.lineSeparator() + @@ -510,14 +515,14 @@ private void setupStubs() { @Test public void testName() { - assertThat(new AzureDevOpsPullRequestDecorator(mock(Server.class), mock(ScmInfoRepository.class), mock(AzureDevopsClientFactory.class)).alm()).isEqualTo(Collections.singletonList(ALM.AZURE_DEVOPS)); + assertThat(new AzureDevOpsPullRequestDecorator(mock(ScmInfoRepository.class), mock(AzureDevopsClientFactory.class), mock(ReportGenerator.class), mock(MarkdownFormatterFactory.class)).alm()).isEqualTo(Collections.singletonList(ALM.AZURE_DEVOPS)); } @Test public void testDecorateQualityGateRepoNameException() { when(almSettingDto.getUrl()).thenReturn("almUrl"); when(almSettingDto.getDecryptedPersonalAccessToken(any())).thenReturn("personalAccessToken"); - when(analysisDetails.getBranchName()).thenReturn("123"); + when(analysisDetails.getPullRequestId()).thenReturn("123"); when(projectAlmSettingDto.getAlmSlug()).thenReturn("prj"); assertThatThrownBy(() -> pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) @@ -529,7 +534,7 @@ public void testDecorateQualityGateRepoNameException() { public void testDecorateQualityGateRepoSlugException() { when(almSettingDto.getUrl()).thenReturn("almUrl"); when(almSettingDto.getDecryptedPersonalAccessToken(any())).thenReturn("personalAccessToken"); - when(analysisDetails.getBranchName()).thenReturn("123"); + when(analysisDetails.getPullRequestId()).thenReturn("123"); when(projectAlmSettingDto.getAlmRepo()).thenReturn("repo"); assertThatThrownBy(() -> pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) @@ -553,7 +558,7 @@ public void testDecorateQualityGateProjectIDException() { public void testDecorateQualityGatePRBranchException() { when(almSettingDto.getUrl()).thenReturn("almUrl"); when(almSettingDto.getDecryptedPersonalAccessToken(any())).thenReturn("personalAccessToken"); - when(analysisDetails.getBranchName()).thenReturn("NON-NUMERIC"); + when(analysisDetails.getPullRequestId()).thenReturn("NON-NUMERIC"); when(projectAlmSettingDto.getAlmSlug()).thenReturn("prj"); when(projectAlmSettingDto.getAlmRepo()).thenReturn("repo"); @@ -581,4 +586,24 @@ public void decorateQualityGateStatusClosedIssue() { assertThat(result.getPullRequestUrl()).isEqualTo(Optional.of(String.format("%s/%s/_git/%s/pullRequest/%s", wireMockRule.baseUrl(), azureProject, azureRepository, pullRequestId))); } + @Test + public void shouldRemoveUserInfoFromRepositoryUrlForLinking() { + ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); + AzureDevopsClientFactory azureDevopsClientFactory = mock(AzureDevopsClientFactory.class); + ReportGenerator reportGenerator = mock(ReportGenerator.class); + MarkdownFormatterFactory markdownFormatterFactory = mock(MarkdownFormatterFactory.class); + + AzureDevOpsPullRequestDecorator underTest = new AzureDevOpsPullRequestDecorator(scmInfoRepository, azureDevopsClientFactory, reportGenerator, markdownFormatterFactory); + + Repository repository = mock(Repository.class); + when(repository.getRemoteUrl()).thenReturn("https://user@domain.com/path/to/repo"); + PullRequest pullRequest = mock(PullRequest.class); + when(pullRequest.getRepository()).thenReturn(repository); + when(pullRequest.getId()).thenReturn(999); + + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + + assertThat(underTest.createFrontEndUrl(pullRequest, analysisDetails)).contains("https://domain.com/path/to/repo/pullRequest/999"); + } + } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java index 941797425..194bd2ef3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecoratorTest.java @@ -3,15 +3,21 @@ import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.BitbucketClient; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.BitbucketClientFactory; import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.AnnotationUploadLimit; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.DataValue; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportData; +import com.github.mc1arke.sonarqube.plugin.almclient.bitbucket.model.ReportStatus; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.issue.Issue; -import org.sonar.api.measures.CoreMetrics; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.ce.task.projectanalysis.component.Component; @@ -20,26 +26,24 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; +import java.math.BigDecimal; import java.time.Instant; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.Optional; -import static org.junit.Assert.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) -public class BitbucketPullRequestDecoratorTest { +class BitbucketPullRequestDecoratorTest { - private static final String PROJECT = "project"; - private static final String REPO = "repo"; private static final String COMMIT = "commit"; + private static final String REPORT_KEY = "report-key"; private static final String ISSUE_KEY = "issue-key"; private static final int ISSUE_LINE = 1; @@ -50,66 +54,81 @@ public class BitbucketPullRequestDecoratorTest { private static final String IMAGE_URL = "https://image-url"; private final AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + private final ReportGenerator reportGenerator = mock(ReportGenerator.class); private final BitbucketClient client = mock(BitbucketClient.class); private final BitbucketClientFactory bitbucketClientFactory = mock(BitbucketClientFactory.class); - private final BitbucketPullRequestDecorator underTest = new BitbucketPullRequestDecorator(bitbucketClientFactory); + private final BitbucketPullRequestDecorator underTest = new BitbucketPullRequestDecorator(bitbucketClientFactory, reportGenerator); private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class); private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + private final AnalysisSummary analysisSummary = mock(AnalysisSummary.class); - @Before - public void setUp() { + @BeforeEach + void setUp() { when(bitbucketClientFactory.createClient(any(), any())).thenReturn(client); - when(client.resolveProject(any(), any())).thenReturn(PROJECT); - when(client.resolveRepository(any(), any())).thenReturn(REPO); } @Test - public void testValidAnalysis() throws IOException { + void testValidAnalysis() throws IOException { when(client.supportsCodeInsights()).thenReturn(true); AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(1000, 1000); when(client.getAnnotationUploadLimit()).thenReturn(uploadLimit); mockValidAnalysis(); + when(analysisSummary.getNewDuplications()).thenReturn(BigDecimal.TEN); + when(analysisSummary.getNewCoverage()).thenReturn(BigDecimal.ONE); underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); - verify(client).createCodeInsightsAnnotation(eq(ISSUE_KEY), eq(ISSUE_LINE), eq(ISSUE_LINK), eq(ISSUE_MESSAGE), eq(ISSUE_PATH), eq("HIGH"), eq("BUG")); + ArgumentCaptor> reportDataArgumentCaptor = ArgumentCaptor.forClass(List.class); + verify(client).createCodeInsightsAnnotation(ISSUE_KEY, ISSUE_LINE, ISSUE_LINK, ISSUE_MESSAGE, ISSUE_PATH, "HIGH", "BUG"); verify(client).createLinkDataValue(DASHBOARD_URL); - verify(client).createCodeInsightsReport(any(), eq("Quality Gate passed" + System.lineSeparator()), any(), eq(DASHBOARD_URL), eq(String.format("%s/common/icon.png", IMAGE_URL)), eq(QualityGate.Status.OK)); - verify(client).deleteAnnotations(PROJECT, REPO, COMMIT); + verify(client).createCodeInsightsReport(reportDataArgumentCaptor.capture(), eq("Quality Gate passed" + System.lineSeparator()), any(), eq(DASHBOARD_URL), eq(String.format("%s/common/icon.png", IMAGE_URL)), eq(ReportStatus.PASSED)); + verify(client).deleteAnnotations(COMMIT, REPORT_KEY); + + assertThat(reportDataArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(List.of(new ReportData("Reliability", new DataValue.Text("0 Bugs")), + new ReportData("Code coverage", new DataValue.Percentage(BigDecimal.ONE)), + new ReportData("Security", new DataValue.Text("0 Vulnerabilities (and 0 Hotspots)")), + new ReportData("Duplication", new DataValue.Percentage(BigDecimal.TEN)), + new ReportData("Maintainability", new DataValue.Text("0 Code Smells")), + new ReportData("Analysis details", null))); } @Test - public void testExceedsMaximumNumberOfAnnotations() { - // given - AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(100, 1000); - int counter = 2; - - // when - boolean result = BitbucketPullRequestDecorator.exceedsMaximumNumberOfAnnotations(counter, uploadLimit); - - // then - assertFalse(result); - } - - @Test - public void testExceedsMaximumNumberOfAnnotationsEdgeCase() { - // given + void testNullPercentagesReplacedWithZeroValues() throws IOException { + when(client.supportsCodeInsights()).thenReturn(true); AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(1000, 1000); - int counter = 1; + when(client.getAnnotationUploadLimit()).thenReturn(uploadLimit); - // when - boolean result = BitbucketPullRequestDecorator.exceedsMaximumNumberOfAnnotations(counter, uploadLimit); + mockValidAnalysis(); + when(analysisSummary.getNewCoverage()).thenReturn(null); + when(analysisSummary.getNewDuplications()).thenReturn(null); + underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); - // then - assertFalse(result); + ArgumentCaptor> reportDataArgumentCaptor = ArgumentCaptor.forClass(List.class); + verify(client).createCodeInsightsAnnotation(ISSUE_KEY, ISSUE_LINE, ISSUE_LINK, ISSUE_MESSAGE, ISSUE_PATH, "HIGH", "BUG"); + verify(client).createLinkDataValue(DASHBOARD_URL); + verify(client).createCodeInsightsReport(reportDataArgumentCaptor.capture(), eq("Quality Gate passed" + System.lineSeparator()), any(), eq(DASHBOARD_URL), eq(String.format("%s/common/icon.png", IMAGE_URL)), eq(ReportStatus.PASSED)); + verify(client).deleteAnnotations(COMMIT, REPORT_KEY); + + assertThat(reportDataArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(List.of(new ReportData("Reliability", new DataValue.Text("0 Bugs")), + new ReportData("Code coverage", new DataValue.Percentage(BigDecimal.ZERO)), + new ReportData("Security", new DataValue.Text("0 Vulnerabilities (and 0 Hotspots)")), + new ReportData("Duplication", new DataValue.Percentage(BigDecimal.ZERO)), + new ReportData("Maintainability", new DataValue.Text("0 Code Smells")), + new ReportData("Analysis details", null))); } - @Test - public void testExceedsMaximumNumberOfAnnotationsEdgeBatchCase() { + @ParameterizedTest(name = "{arguments}") + @CsvSource({"100, 1000, 2", + "1000, 1000, 1", + "100, 1000, 10"}) + void testExceedsMaximumNumberOfAnnotations(int annotationBatchSize, int totalAllowedAnnotations, int counter) { // given - AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(100, 1000); - int counter = 10; + AnnotationUploadLimit uploadLimit = new AnnotationUploadLimit(annotationBatchSize, totalAllowedAnnotations); // when boolean result = BitbucketPullRequestDecorator.exceedsMaximumNumberOfAnnotations(counter, uploadLimit); @@ -121,18 +140,9 @@ public void testExceedsMaximumNumberOfAnnotationsEdgeBatchCase() { private void mockValidAnalysis() { when(analysisDetails.getCommitSha()).thenReturn(COMMIT); when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); + when(analysisDetails.getAnalysisProjectKey()).thenReturn(REPORT_KEY); - Map ruleCount = new HashMap<>(); - ruleCount.put(RuleType.CODE_SMELL, 1L); - ruleCount.put(RuleType.VULNERABILITY, 2L); - ruleCount.put(RuleType.SECURITY_HOTSPOT, 3L); - ruleCount.put(RuleType.BUG, 4L); - - when(analysisDetails.countRuleByType()).thenReturn(ruleCount); - when(analysisDetails.findQualityGateCondition(CoreMetrics.NEW_COVERAGE_KEY)).thenReturn(Optional.empty()); - when(analysisDetails.findQualityGateCondition(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY)).thenReturn(Optional.empty()); when(analysisDetails.getAnalysisDate()).thenReturn(Date.from(Instant.now())); - when(analysisDetails.getDashboardUrl()).thenReturn(DASHBOARD_URL); ReportAttributes reportAttributes = mock(ReportAttributes.class); when(reportAttributes.getScmPath()).thenReturn(Optional.of(ISSUE_PATH)); @@ -148,17 +158,20 @@ private void mockValidAnalysis() { when(defaultIssue.key()).thenReturn(ISSUE_KEY); when(defaultIssue.type()).thenReturn(RuleType.BUG); when(defaultIssue.getMessage()).thenReturn(ISSUE_MESSAGE); - when(analysisDetails.getIssueUrl(defaultIssue)).thenReturn(ISSUE_LINK); - when(analysisDetails.getBaseImageUrl()).thenReturn(IMAGE_URL); PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); when(componentIssue.getIssue()).thenReturn(defaultIssue); when(componentIssue.getComponent()).thenReturn(component); - PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + AnalysisIssueSummary analysisIssueSummary = mock(AnalysisIssueSummary.class); + when(analysisIssueSummary.getIssueUrl()).thenReturn("https://issue-link"); + when(reportGenerator.createAnalysisIssueSummary(any(), any())).thenReturn(analysisIssueSummary); + + when(analysisSummary.getDashboardUrl()).thenReturn("https://dashboard-url"); + when(analysisSummary.getSummaryImageUrl()).thenReturn("https://image-url/common/icon.png"); + when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); + when(analysisDetails.getScmReportableIssues()).thenReturn(List.of(componentIssue)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java index 2827ef584..10f595b90 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,47 +20,96 @@ import com.github.mc1arke.sonarqube.plugin.almclient.github.GithubClient; import com.github.mc1arke.sonarqube.plugin.almclient.github.GithubClientFactory; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.Annotation; +import com.github.mc1arke.sonarqube.plugin.almclient.github.model.CheckRunDetails; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckAnnotationLevel; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v4.model.CheckConclusionState; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; -import org.junit.Test; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.rule.Severity; +import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; import java.util.Collections; +import java.util.Date; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -public class GithubPullRequestDecoratorTest { +class GithubPullRequestDecoratorTest { private final GithubClient githubClient = mock(GithubClient.class); private final AnalysisDetails analysisDetails = mock(AnalysisDetails.class); private final GithubClientFactory githubClientFactory = mock(GithubClientFactory.class); - private final GithubPullRequestDecorator testCase = new GithubPullRequestDecorator(githubClientFactory); + private final ReportGenerator reportGenerator = mock(ReportGenerator.class); + private final MarkdownFormatterFactory markdownFormatterFactory = mock(MarkdownFormatterFactory.class); + private final Clock clock = Clock.fixed(Instant.ofEpochSecond(102030405), ZoneId.of("UTC")); + private final GithubPullRequestDecorator testCase = new GithubPullRequestDecorator(githubClientFactory, reportGenerator, markdownFormatterFactory, clock); private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + private final AnalysisSummary analysisSummary = mock(AnalysisSummary.class); + @BeforeEach + void setUp() { + doReturn("123").when(analysisDetails).getPullRequestId(); + doReturn(Date.from(clock.instant())).when(analysisDetails).getAnalysisDate(); + doReturn("analysis-id").when(analysisDetails).getAnalysisId(); + doReturn("project-key").when(analysisDetails).getAnalysisProjectKey(); + doReturn("Project Name").when(analysisDetails).getAnalysisProjectName(); + doReturn(QualityGate.Status.OK).when(analysisDetails).getQualityGateStatus(); + doReturn("commit-sha").when(analysisDetails).getCommitSha(); + doReturn(IntStream.range(0, 20).mapToObj(i -> { + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + Component component = mock(Component.class); + doReturn(Optional.of("path" + i)).when(componentIssue).getScmPath(); + doReturn(component).when(componentIssue).getComponent(); + PostAnalysisIssueVisitor.LightIssue lightIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn("issue message " + i).when(lightIssue).getMessage(); + doReturn(i).when(lightIssue).getLine(); + doReturn(Severity.ALL.get(i % Severity.ALL.size())).when(lightIssue).severity(); + doReturn(lightIssue).when(componentIssue).getIssue(); + return componentIssue; + }).collect(Collectors.toList())).when(analysisDetails).getScmReportableIssues(); + + doReturn(analysisSummary).when(reportGenerator).createAnalysisSummary(any()); + doReturn("dashboard-url").when(analysisSummary).getDashboardUrl(); + doReturn("report summary").when(analysisSummary).format(any()); + } @Test - public void testName() { + void verifyCorrectNameReturned() { assertThat(testCase.alm()).isEqualTo(Collections.singletonList(ALM.GITHUB)); } @Test - public void testDecorateQualityGatePropagateException() throws IOException { + void verifyClientExceptionPropagated() throws IOException { Exception dummyException = new IOException("Dummy Exception"); doReturn(githubClient).when(githubClientFactory).createClient(any(), any()); - doThrow(dummyException).when(githubClient).createCheckRun(any(), any(), any()); + doThrow(dummyException).when(githubClient).createCheckRun(any(), anyBoolean()); assertThatThrownBy(() -> testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) .hasMessage("Could not decorate Pull Request on Github") @@ -68,15 +117,37 @@ public void testDecorateQualityGatePropagateException() throws IOException { } @Test - public void testDecorateQualityGateReturnValue() throws IOException { - DecorationResult expectedResult = DecorationResult.builder().build(); + void verifyCorrectArgumentsAndReturnValuesUsed() throws IOException { + doReturn(true).when(projectAlmSettingDto).getSummaryCommentEnabled(); + DecorationResult expectedResult = DecorationResult.builder().withPullRequestUrl("http://github.url/repo/path/pull/123").build(); doReturn(githubClient).when(githubClientFactory).createClient(any(), any()); - doReturn(expectedResult).when(githubClient).createCheckRun(any(), any(), any()); + doReturn("checkRunId").when(githubClient).createCheckRun(any(), anyBoolean()); + doReturn("http://github.url/repo/path").when(githubClient).getRepositoryUrl(); DecorationResult decorationResult = testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); - verify(githubClient).createCheckRun(argumentCaptor.capture(), eq(almSettingDto), eq(projectAlmSettingDto)); - assertEquals(analysisDetails, argumentCaptor.getValue()); - assertThat(decorationResult).isSameAs(expectedResult); + ArgumentCaptor checkRunDetailsArgumentCaptor = ArgumentCaptor.forClass(CheckRunDetails.class); + verify(githubClient).createCheckRun(checkRunDetailsArgumentCaptor.capture(), eq(true)); + + assertThat(checkRunDetailsArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(CheckRunDetails.builder() + .withTitle("Quality Gate success") + .withName("Project Name Sonarqube Results") + .withExternalId("analysis-id") + .withPullRequestId(123) + .withStartTime(clock.instant().atZone(ZoneId.of("UTC"))) + .withEndTime(clock.instant().atZone(ZoneId.of("UTC"))) + .withDashboardUrl("dashboard-url") + .withSummary("report summary") + .withCommitId("commit-sha") + .withAnnotations(IntStream.range(0, 20).mapToObj(i -> Annotation.builder() + .withScmPath("path" + i) + .withLine(i) + .withMessage("issue message " + i) + .withSeverity(i % 5 < 1 ? CheckAnnotationLevel.NOTICE : i % 5 > 2 ? CheckAnnotationLevel.FAILURE : CheckAnnotationLevel.WARNING) + .build()).collect(Collectors.toList())) + .withCheckConclusionState(CheckConclusionState.SUCCESS) + .build()); + assertThat(decorationResult).usingRecursiveComparison().isEqualTo(expectedResult); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorIntegrationTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorIntegrationTest.java index bb078e505..a18fa8798 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorIntegrationTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 Markus Heberling, Michael Clarke + * Copyright (C) 2019-2022 Markus Heberling, Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -22,6 +22,10 @@ import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Rule; import org.junit.Test; @@ -29,7 +33,6 @@ import org.sonar.api.config.internal.Encryption; import org.sonar.api.config.internal.Settings; import org.sonar.api.issue.Issue; -import org.sonar.api.platform.Server; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.ScmInfo; @@ -63,7 +66,7 @@ public class GitlabMergeRequestDecoratorIntegrationTest { @Rule - public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig()); + public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort()); @Test public void decorateQualityGateStatusOk() { @@ -99,10 +102,8 @@ private void decorateQualityGateStatus(QualityGate.Status status) { when(projectAlmSettingDto.getAlmRepo()).thenReturn(repositorySlug); when(analysisDetails.getQualityGateStatus()).thenReturn(status); when(analysisDetails.getAnalysisProjectKey()).thenReturn(projectKey); - when(analysisDetails.getBranchName()).thenReturn(Long.toString(mergeRequestIid)); + when(analysisDetails.getPullRequestId()).thenReturn(Long.toString(mergeRequestIid)); when(analysisDetails.getCommitSha()).thenReturn(commitSHA); - when(analysisDetails.getCoverage()).thenReturn(Optional.of(BigDecimal.TEN)); - PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); @@ -116,7 +117,7 @@ private void decorateQualityGateStatus(QualityGate.Status status) { when(componentIssue.getIssue()).thenReturn(defaultIssue); Component component = mock(Component.class); when(componentIssue.getComponent()).thenReturn(component); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); + when(componentIssue.getScmPath()).thenReturn(Optional.of(filePath)); ScmInfo scmInfo = mock(ScmInfo.class); when(scmInfo.hasChangesetForLine(anyInt())).thenReturn(true); @@ -128,11 +129,18 @@ private void decorateQualityGateStatus(QualityGate.Status status) { issues.add(componentIssue); } - when(issueVisitor.getIssues()).thenReturn(issues); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("summary commént\n\n[link text]"); - when(analysisDetails.createAnalysisIssueSummary(any(), any())).thenReturn("issué"); - when(analysisDetails.parseIssueIdFromUrl(any())).thenCallRealMethod(); + when(analysisDetails.getScmReportableIssues()).thenReturn(issues); + + ReportGenerator reportGenerator = mock(ReportGenerator.class); + AnalysisSummary analysisSummary = mock(AnalysisSummary.class); + when(analysisSummary.getNewCoverage()).thenReturn(BigDecimal.TEN); + when(analysisSummary.getDashboardUrl()).thenReturn(sonarRootUrl + "/dashboard?id=" + projectKey + "&pullRequest=" + mergeRequestIid); + when(analysisSummary.format(any())).thenReturn("summary commént\n\n[link text]"); + when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary); + + AnalysisIssueSummary analysisIssueSummary = mock(AnalysisIssueSummary.class); + when(analysisIssueSummary.format(any())).thenReturn("issué"); + when(reportGenerator.createAnalysisIssueSummary(any(), any())).thenReturn(analysisIssueSummary); wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/user")).withHeader("PRIVATE-TOKEN", equalTo("token")).willReturn(okJson("{\n" + " \"id\": 1,\n" + @@ -141,6 +149,7 @@ private void decorateQualityGateStatus(QualityGate.Status status) { wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + mergeRequestIid)).willReturn(okJson("{\n" + " \"id\": 15235,\n" + " \"iid\": " + mergeRequestIid + ",\n" + + " \"target_project_id\": " + sourceProjectId + ",\n" + " \"web_url\": \"http://gitlab.example.com/my-group/my-project/merge_requests/1\",\n" + " \"diff_refs\": {\n" + " \"base_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\",\n" + @@ -227,13 +236,11 @@ private void decorateQualityGateStatus(QualityGate.Status status) { ); LinkHeaderReader linkHeaderReader = mock(LinkHeaderReader.class); - Server server = mock(Server.class); - when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); Settings settings = mock(Settings.class); Encryption encryption = mock(Encryption.class); when(settings.getEncryption()).thenReturn(encryption); GitlabMergeRequestDecorator pullRequestDecorator = - new GitlabMergeRequestDecorator(server, scmInfoRepository, new DefaultGitlabClientFactory(linkHeaderReader, settings)); + new GitlabMergeRequestDecorator(scmInfoRepository, new DefaultGitlabClientFactory(linkHeaderReader, settings), reportGenerator, mock(MarkdownFormatterFactory.class)); assertThat(pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto).getPullRequestUrl()).isEqualTo(Optional.of("http://gitlab.example.com/my-group/my-project/merge_requests/1")); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorTest.java index 956c40328..009e7dab8 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabMergeRequestDecoratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,9 +20,6 @@ import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.GitlabClient; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.GitlabClientFactory; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; -import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.model.Commit; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.model.CommitNote; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.model.DiffRefs; @@ -32,12 +29,18 @@ import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.model.Note; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.model.PipelineStatus; import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.model.User; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.MarkdownFormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisIssueSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.AnalysisSummary; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report.ReportGenerator; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.issue.Issue; -import org.sonar.api.platform.Server; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.ScmInfo; @@ -85,26 +88,33 @@ public class GitlabMergeRequestDecoratorTest { private final GitlabClient gitlabClient = mock(GitlabClient.class); private final GitlabClientFactory gitlabClientFactory = mock(GitlabClientFactory.class); - private final Server server = mock(Server.class); private final ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); private final AnalysisDetails analysisDetails = mock(AnalysisDetails.class); private final AlmSettingDto almSettingDto = mock(AlmSettingDto.class); private final ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); private final MergeRequest mergeRequest = mock(MergeRequest.class); private final User sonarqubeUser = mock(User.class); - private final PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); private final DiffRefs diffRefs = mock(DiffRefs.class); + private final ReportGenerator reportGenerator = mock(ReportGenerator.class); + private final MarkdownFormatterFactory markdownFormatterFactory = mock(MarkdownFormatterFactory.class); + private final AnalysisSummary analysisSummary = mock(AnalysisSummary.class); - private final GitlabMergeRequestDecorator underTest = new GitlabMergeRequestDecorator(server, scmInfoRepository, gitlabClientFactory); + private final GitlabMergeRequestDecorator underTest = new GitlabMergeRequestDecorator(scmInfoRepository, gitlabClientFactory, reportGenerator, markdownFormatterFactory); @Before public void setUp() throws IOException { + when(analysisSummary.format(any())).thenReturn("Summary Comment"); + when(reportGenerator.createAnalysisSummary(any())).thenReturn(analysisSummary); + AnalysisIssueSummary analysisIssueSummary = mock(AnalysisIssueSummary.class); + when(analysisIssueSummary.format(any())).thenReturn("Issue Summary"); + when(reportGenerator.createAnalysisIssueSummary(any(), any())).thenReturn(analysisIssueSummary); when(gitlabClientFactory.createClient(any(), any())).thenReturn(gitlabClient); when(almSettingDto.getUrl()).thenReturn("http://gitlab.dummy"); when(projectAlmSettingDto.getAlmRepo()).thenReturn(PROJECT_PATH); - when(analysisDetails.getBranchName()).thenReturn(Long.toString(MERGE_REQUEST_IID)); + when(analysisDetails.getPullRequestId()).thenReturn(Long.toString(MERGE_REQUEST_IID)); when(mergeRequest.getIid()).thenReturn(MERGE_REQUEST_IID); when(mergeRequest.getSourceProjectId()).thenReturn(PROJECT_ID); + when(mergeRequest.getTargetProjectId()).thenReturn(PROJECT_ID); when(mergeRequest.getDiffRefs()).thenReturn(diffRefs); when(mergeRequest.getWebUrl()).thenReturn(MERGE_REQUEST_WEB_URL); when(diffRefs.getBaseSha()).thenReturn(BASE_SHA); @@ -116,10 +126,9 @@ public void setUp() throws IOException { .collect(Collectors.toList())); when(sonarqubeUser.getUsername()).thenReturn(SONARQUBE_USERNAME); when(gitlabClient.getCurrentUser()).thenReturn(sonarqubeUser); - when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(postAnalysisIssueVisitor); when(analysisDetails.getAnalysisProjectKey()).thenReturn(PROJECT_KEY); when(analysisDetails.getAnalysisId()).thenReturn(ANALYSIS_UUID); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(new ArrayList<>()); + when(analysisDetails.getScmReportableIssues()).thenReturn(new ArrayList<>()); } @Test @@ -129,7 +138,7 @@ public void shouldReturnCorrectDecoratorType() { @Test public void shouldThrowErrorWhenPullRequestKeyNotNumeric() { - when(analysisDetails.getBranchName()).thenReturn("non-MR-IID"); + when(analysisDetails.getPullRequestId()).thenReturn("non-MR-IID"); assertThatThrownBy(() -> underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) .isInstanceOf(IllegalStateException.class) @@ -246,7 +255,7 @@ public void shouldCloseDiscussionWithResolvableNoteFromSonarqubeUserAndOnlySyste Note note = mock(Note.class); when(note.getAuthor()).thenReturn(sonarqubeUser); - when(note.getBody()).thenReturn("[View in SonarQube](url)"); + when(note.getBody()).thenReturn("[View in SonarQube](http://host.domain/issue?issues=issueId&id=" + PROJECT_KEY + ")"); when(note.isResolvable()).thenReturn(true); Note note2 = mock(Note.class); @@ -259,7 +268,6 @@ public void shouldCloseDiscussionWithResolvableNoteFromSonarqubeUserAndOnlySyste when(discussion.getId()).thenReturn("discussionId2"); when(discussion.getNotes()).thenReturn(Arrays.asList(note, note2)); - when(analysisDetails.parseIssueIdFromUrl("url")).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier(PROJECT_KEY, "issueId"))); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(Collections.singletonList(discussion)); underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); @@ -337,7 +345,7 @@ public void shouldCommentAboutCloseOfDiscussionWithMultipleResolvableNotesFromSo Note note = mock(Note.class); when(note.getAuthor()).thenReturn(sonarqubeUser); - when(note.getBody()).thenReturn("Sonarqube reported issue\n[View in SonarQube](https://dummy.url.with.subdomain/path/to/sonarqube?paramters=many&values=complex%20and+encoded)"); + when(note.getBody()).thenReturn("Sonarqube reported issue\n[View in SonarQube](https://dummy.url.with.subdomain/path/to/sonarqube?paramters=many&values=complex%20and+encoded&issues=new-issue&id=" + PROJECT_KEY + ")"); when(note.isResolvable()).thenReturn(true); Note note2 = mock(Note.class); @@ -349,7 +357,6 @@ public void shouldCommentAboutCloseOfDiscussionWithMultipleResolvableNotesFromSo when(discussion.getId()).thenReturn("discussionId5"); when(discussion.getNotes()).thenReturn(Arrays.asList(note, note2)); - when(analysisDetails.parseIssueIdFromUrl("https://dummy.url.with.subdomain/path/to/sonarqube?paramters=many&values=complex%20and+encoded")).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier(PROJECT_KEY, "new-issue"))); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(Collections.singletonList(discussion)); underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); @@ -370,7 +377,7 @@ public void shouldThrowErrorIfUnableToCleanUpDiscussionOnGitlab() throws IOExcep Note note = mock(Note.class); when(note.getAuthor()).thenReturn(sonarqubeUser); - when(note.getBody()).thenReturn("Sonarqube reported issue\n[View in SonarQube](https://dummy.url.with.subdomain/path/to/sonarqube?paramters=many&values=complex%20and+encoded)"); + when(note.getBody()).thenReturn("Sonarqube reported issue\n[View in SonarQube](https://dummy.url.with.subdomain/path/to/sonarqube?paramters=many&values=complex%20and+encoded&issues=issuedId&id=" + PROJECT_KEY + ")"); when(note.isResolvable()).thenReturn(true); Note note2 = mock(Note.class); @@ -382,7 +389,6 @@ public void shouldThrowErrorIfUnableToCleanUpDiscussionOnGitlab() throws IOExcep when(discussion.getId()).thenReturn("discussionId5"); when(discussion.getNotes()).thenReturn(Arrays.asList(note, note2)); - when(analysisDetails.parseIssueIdFromUrl("https://dummy.url.with.subdomain/path/to/sonarqube?paramters=many&values=complex%20and+encoded")).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier(PROJECT_KEY, "issueId"))); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(Collections.singletonList(discussion)); doThrow(new IOException("dummy")).when(gitlabClient).addMergeRequestDiscussionNote(anyLong(), anyLong(), any(), any()); @@ -420,7 +426,6 @@ public void shouldNotCommentOrAttemptCloseOfDiscussionWithMultipleResolvableNote when(discussion.getId()).thenReturn("discussionId6"); when(discussion.getNotes()).thenReturn(Arrays.asList(note, note2, note3)); - when(analysisDetails.parseIssueIdFromUrl("url")).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier(PROJECT_KEY, "issueId"))); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(Collections.singletonList(discussion)); underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); @@ -450,7 +455,6 @@ public void shouldNotCommentOrAttemptCloseOfDiscussionWithMultipleResolvableNote when(discussion.getId()).thenReturn("discussionId6"); when(discussion.getNotes()).thenReturn(Arrays.asList(note, note2, note3)); - when(analysisDetails.parseIssueIdFromUrl("url")).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier("otherProjectKey", "issueId"))); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(Collections.singletonList(discussion)); underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); @@ -471,11 +475,10 @@ public void shouldThrowErrorIfSubmittingNewIssueToGitlabFails() throws IOExcepti PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); when(componentIssue.getIssue()).thenReturn(lightIssue); when(componentIssue.getComponent()).thenReturn(component); + when(componentIssue.getScmPath()).thenReturn(Optional.of("path-to-file")); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + when(analysisDetails.getScmReportableIssues()).thenReturn(Collections.singletonList(componentIssue)); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(new ArrayList<>()); - when(analysisDetails.createAnalysisIssueSummary(eq(componentIssue), any())).thenReturn("Issue Summary"); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of("path-to-file")); Changeset changeset = mock(Changeset.class); when(changeset.getRevision()).thenReturn("DEF"); @@ -497,7 +500,9 @@ public void shouldThrowErrorIfSubmittingNewIssueToGitlabFails() throws IOExcepti ArgumentCaptor mergeRequestNoteArgumentCaptor = ArgumentCaptor.forClass(MergeRequestNote.class); verify(gitlabClient).addMergeRequestDiscussion(eq(PROJECT_ID), eq(MERGE_REQUEST_IID), mergeRequestNoteArgumentCaptor.capture()); - assertThat(mergeRequestNoteArgumentCaptor.getValue()).isEqualToComparingFieldByField(new CommitNote("Issue Summary", BASE_SHA, START_SHA, HEAD_SHA, "path-to-file", "path-to-file", 999)); + assertThat(mergeRequestNoteArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(new CommitNote("Issue Summary", BASE_SHA, START_SHA, HEAD_SHA, "path-to-file", "path-to-file", 999)); } @Test @@ -512,11 +517,10 @@ public void shouldStartNewDiscussionForNewIssueFromCommitInMergeRequest() throws PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); when(componentIssue.getIssue()).thenReturn(lightIssue); when(componentIssue.getComponent()).thenReturn(component); + when(componentIssue.getScmPath()).thenReturn(Optional.of("path-to-file")); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + when(analysisDetails.getScmReportableIssues()).thenReturn(Collections.singletonList(componentIssue)); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(new ArrayList<>()); - when(analysisDetails.createAnalysisIssueSummary(eq(componentIssue), any())).thenReturn("Issue Summary"); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of("path-to-file")); Changeset changeset = mock(Changeset.class); when(changeset.getRevision()).thenReturn("DEF"); @@ -534,7 +538,9 @@ public void shouldStartNewDiscussionForNewIssueFromCommitInMergeRequest() throws ArgumentCaptor mergeRequestNoteArgumentCaptor = ArgumentCaptor.forClass(MergeRequestNote.class); verify(gitlabClient, times(2)).addMergeRequestDiscussion(eq(PROJECT_ID), eq(MERGE_REQUEST_IID), mergeRequestNoteArgumentCaptor.capture()); - assertThat(mergeRequestNoteArgumentCaptor.getAllValues().get(0)).isEqualToComparingFieldByField(new CommitNote("Issue Summary", BASE_SHA, START_SHA, HEAD_SHA, "path-to-file", "path-to-file", 999)); + assertThat(mergeRequestNoteArgumentCaptor.getAllValues().get(0)) + .usingRecursiveComparison() + .isEqualTo(new CommitNote("Issue Summary", BASE_SHA, START_SHA, HEAD_SHA, "path-to-file", "path-to-file", 999)); assertThat(mergeRequestNoteArgumentCaptor.getAllValues().get(1)).isNotInstanceOf(CommitNote.class); } @@ -550,9 +556,10 @@ public void shouldNotStartNewDiscussionForIssueWithExistingCommentFromCommitInMe PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); when(componentIssue.getIssue()).thenReturn(lightIssue); when(componentIssue.getComponent()).thenReturn(component); + when(componentIssue.getScmPath()).thenReturn(Optional.of("path-to-file")); Note note = mock(Note.class); - when(note.getBody()).thenReturn("Reported issue\n[View in SonarQube](url)"); + when(note.getBody()).thenReturn("Reported issue\n[View in SonarQube](http://domain.url/sonar/issue?issues=issueKey1&id=" + PROJECT_KEY + ")"); when(note.getAuthor()).thenReturn(sonarqubeUser); when(note.isResolvable()).thenReturn(true); @@ -560,11 +567,8 @@ public void shouldNotStartNewDiscussionForIssueWithExistingCommentFromCommitInMe when(discussion.getId()).thenReturn("discussion-id"); when(discussion.getNotes()).thenReturn(Collections.singletonList(note)); - when(analysisDetails.parseIssueIdFromUrl("url")).thenReturn(Optional.of(new AnalysisDetails.ProjectIssueIdentifier(PROJECT_KEY, "issueKey1"))); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(Collections.singletonList(discussion)); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); - when(analysisDetails.createAnalysisIssueSummary(eq(componentIssue), any())).thenReturn("Issue Summary"); - when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of("path-to-file")); + when(analysisDetails.getScmReportableIssues()).thenReturn(Collections.singletonList(componentIssue)); Changeset changeset = mock(Changeset.class); when(changeset.getRevision()).thenReturn("DEF"); @@ -598,9 +602,8 @@ public void shouldNotCreateCommentsForIssuesWithNoLineNumbers() throws IOExcepti when(componentIssue.getIssue()).thenReturn(lightIssue); when(componentIssue.getComponent()).thenReturn(component); - when(postAnalysisIssueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + when(analysisDetails.getScmReportableIssues()).thenReturn(Collections.singletonList(componentIssue)); when(gitlabClient.getMergeRequestDiscussions(anyLong(), anyLong())).thenReturn(new ArrayList<>()); - when(analysisDetails.createAnalysisIssueSummary(eq(componentIssue), any())).thenReturn("Issue Summary"); underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); @@ -617,10 +620,10 @@ public void shouldNotCreateCommentsForIssuesWithNoLineNumbers() throws IOExcepti @Test public void shouldSubmitSuccessfulPipelineStatusAndResolvedSummaryCommentOnSuccessAnalysis() throws IOException { when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.OK); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("Summary comment"); when(analysisDetails.getCommitSha()).thenReturn("commitsha"); - when(server.getPublicRootUrl()).thenReturn("https://sonarqube.dummy"); + when(analysisSummary.format(any())).thenReturn("Summary comment"); + when(analysisSummary.getDashboardUrl()).thenReturn("https://sonarqube.dummy/dashboard?id=projectKey&pullRequest=123"); Discussion discussion = mock(Discussion.class); when(discussion.getId()).thenReturn("dicussion id"); @@ -634,21 +637,24 @@ public void shouldSubmitSuccessfulPipelineStatusAndResolvedSummaryCommentOnSucce ArgumentCaptor pipelineStatusArgumentCaptor = ArgumentCaptor.forClass(PipelineStatus.class); verify(gitlabClient).setMergeRequestPipelineStatus(eq(PROJECT_ID), eq("commitsha"), pipelineStatusArgumentCaptor.capture()); - assertThat(mergeRequestNoteArgumentCaptor.getValue()).isEqualToComparingFieldByField(new MergeRequestNote("Summary comment")); + assertThat(mergeRequestNoteArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(new MergeRequestNote("Summary comment")); assertThat(pipelineStatusArgumentCaptor.getValue()) - .isEqualToComparingFieldByField(new PipelineStatus("SonarQube", "SonarQube Status", + .usingRecursiveComparison() + .isEqualTo(new PipelineStatus("SonarQube", "SonarQube Status", PipelineStatus.State.SUCCESS, "https://sonarqube.dummy/dashboard?id=" + PROJECT_KEY + "&pullRequest=" + MERGE_REQUEST_IID, null, null)); } @Test public void shouldSubmitFailedPipelineStatusAndUnresolvedSummaryCommentOnFailedAnalysis() throws IOException { when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.ERROR); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("Different Summary comment"); when(analysisDetails.getCommitSha()).thenReturn("other sha"); - when(analysisDetails.getCoverage()).thenReturn(Optional.of(BigDecimal.TEN)); when(analysisDetails.getScannerProperty("com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.pipelineId")).thenReturn(Optional.of("11")); - when(server.getPublicRootUrl()).thenReturn("https://sonarqube2.dummy"); + when(analysisSummary.format(any())).thenReturn("Different Summary comment"); + when(analysisSummary.getDashboardUrl()).thenReturn("https://sonarqube2.dummy/dashboard?id=projectKey&pullRequest=123"); + when(analysisSummary.getNewCoverage()).thenReturn(BigDecimal.TEN); Discussion discussion = mock(Discussion.class); when(discussion.getId()).thenReturn("dicussion id 2"); @@ -662,21 +668,24 @@ public void shouldSubmitFailedPipelineStatusAndUnresolvedSummaryCommentOnFailedA ArgumentCaptor pipelineStatusArgumentCaptor = ArgumentCaptor.forClass(PipelineStatus.class); verify(gitlabClient).setMergeRequestPipelineStatus(eq(PROJECT_ID), eq("other sha"), pipelineStatusArgumentCaptor.capture()); - assertThat(mergeRequestNoteArgumentCaptor.getValue()).isEqualToComparingFieldByField(new MergeRequestNote("Different Summary comment")); + assertThat(mergeRequestNoteArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(new MergeRequestNote("Different Summary comment")); assertThat(pipelineStatusArgumentCaptor.getValue()) - .isEqualToComparingFieldByField(new PipelineStatus("SonarQube", "SonarQube Status", + .usingRecursiveComparison() + .isEqualTo(new PipelineStatus("SonarQube", "SonarQube Status", PipelineStatus.State.FAILED, "https://sonarqube2.dummy/dashboard?id=" + PROJECT_KEY + "&pullRequest=" + MERGE_REQUEST_IID, BigDecimal.TEN, 11L)); } @Test public void shouldThrowErrorWhenSubmitPipelineStatusToGitlabFails() throws IOException { when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.ERROR); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("Different Summary comment"); when(analysisDetails.getCommitSha()).thenReturn("other sha"); - when(analysisDetails.getCoverage()).thenReturn(Optional.of(BigDecimal.TEN)); when(analysisDetails.getScannerProperty("com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.pipelineId")).thenReturn(Optional.of("11")); - when(server.getPublicRootUrl()).thenReturn("https://sonarqube2.dummy"); + when(analysisSummary.format(any())).thenReturn("Different Summary comment"); + when(analysisSummary.getDashboardUrl()).thenReturn("https://sonarqube2.dummy/dashboard?id=projectKey&pullRequest=123"); + when(analysisSummary.getNewCoverage()).thenReturn(BigDecimal.TEN); Discussion discussion = mock(Discussion.class); when(discussion.getId()).thenReturn("dicussion id 2"); @@ -693,21 +702,22 @@ public void shouldThrowErrorWhenSubmitPipelineStatusToGitlabFails() throws IOExc ArgumentCaptor pipelineStatusArgumentCaptor = ArgumentCaptor.forClass(PipelineStatus.class); verify(gitlabClient).setMergeRequestPipelineStatus(eq(PROJECT_ID), eq("other sha"), pipelineStatusArgumentCaptor.capture()); - assertThat(mergeRequestNoteArgumentCaptor.getValue()).isEqualToComparingFieldByField(new MergeRequestNote("Different Summary comment")); + assertThat(mergeRequestNoteArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(new MergeRequestNote("Different Summary comment")); assertThat(pipelineStatusArgumentCaptor.getValue()) - .isEqualToComparingFieldByField(new PipelineStatus("SonarQube", "SonarQube Status", + .usingRecursiveComparison() + .isEqualTo(new PipelineStatus("SonarQube", "SonarQube Status", PipelineStatus.State.FAILED, "https://sonarqube2.dummy/dashboard?id=" + PROJECT_KEY + "&pullRequest=" + MERGE_REQUEST_IID, BigDecimal.TEN, 11L)); } @Test public void shouldThrowErrorWhenSubmitAnalysisToGitlabFails() throws IOException { when(analysisDetails.getQualityGateStatus()).thenReturn(QualityGate.Status.ERROR); - when(analysisDetails.createAnalysisSummary(any())).thenReturn("Different Summary comment"); when(analysisDetails.getCommitSha()).thenReturn("other sha"); - when(analysisDetails.getCoverage()).thenReturn(Optional.of(BigDecimal.TEN)); when(analysisDetails.getScannerProperty("com.github.mc1arke.sonarqube.plugin.branch.pullrequest.gitlab.pipelineId")).thenReturn(Optional.of("11")); - when(server.getPublicRootUrl()).thenReturn("https://sonarqube2.dummy"); + when(analysisSummary.format(any())).thenReturn("Different Summary comment"); Discussion discussion = mock(Discussion.class); when(discussion.getId()).thenReturn("dicussion id 2"); @@ -723,19 +733,23 @@ public void shouldThrowErrorWhenSubmitAnalysisToGitlabFails() throws IOException verify(gitlabClient, never()).resolveMergeRequestDiscussion(PROJECT_ID, MERGE_REQUEST_IID, discussion.getId()); verify(gitlabClient, never()).setMergeRequestPipelineStatus(anyLong(), any(), any()); - assertThat(mergeRequestNoteArgumentCaptor.getValue()).isEqualToComparingFieldByField(new MergeRequestNote("Different Summary comment")); + assertThat(mergeRequestNoteArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(new MergeRequestNote("Different Summary comment")); } @Test public void shouldReturnWebUrlFromMergeRequestIfScannerPropertyNotSet() { assertThat(underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) - .isEqualToComparingFieldByField(DecorationResult.builder().withPullRequestUrl(MERGE_REQUEST_WEB_URL).build()); + .usingRecursiveComparison() + .isEqualTo(DecorationResult.builder().withPullRequestUrl(MERGE_REQUEST_WEB_URL).build()); } @Test public void shouldReturnWebUrlFromScannerPropertyIfSet() { when(analysisDetails.getScannerProperty("sonar.pullrequest.gitlab.projectUrl")).thenReturn(Optional.of(MERGE_REQUEST_WEB_URL + "/additional")); assertThat(underTest.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) - .isEqualToComparingFieldByField(DecorationResult.builder().withPullRequestUrl(MERGE_REQUEST_WEB_URL + "/additional/merge_requests/" + MERGE_REQUEST_IID).build()); + .usingRecursiveComparison() + .isEqualTo(DecorationResult.builder().withPullRequestUrl(MERGE_REQUEST_WEB_URL + "/additional/merge_requests/" + MERGE_REQUEST_IID).build()); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatterTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatterTest.java deleted file mode 100644 index 0ad48e5c7..000000000 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/BaseFormatterTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - */ -package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; - -import org.junit.Test; - -import java.util.Collections; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -public class BaseFormatterTest { - - - @Test - public void checkChildContentsDocument() { - verify(checkFormatInvocation(new Document())).documentFormatter(); - } - - @Test - public void checkChildContentsHeading() { - verify(checkFormatInvocation(new Heading(1))).headingFormatter(); - } - - @Test - public void checkChildContentImage() { - verify(checkFormatInvocation(new Image("", ""))).imageFormatter(); - } - - @Test - public void checkChildContentList() { - verify(checkFormatInvocation(new List(List.Style.BULLET))).listFormatter(); - } - - @Test - public void checkChildContentListItem() { - verify(checkFormatInvocation(new ListItem())).listItemFormatter(); - } - - @Test - public void checkChildContentParagraph() { - verify(checkFormatInvocation(new Paragraph())).paragraphFormatter(); - } - - @Test - public void checkChildContentText() { - verify(checkFormatInvocation(new Text(""))).textFormatter(); - } - - @Test - public void checkChildContentUnkownType() { - Node node = mock(Node.class); - assertThatThrownBy(() -> checkFormatInvocation(mock(Node.class))) - .isExactlyInstanceOf(IllegalArgumentException.class) - .hasMessage("Unknown node type: " + node.getClass().getName()); - } - - - private static FormatterFactory checkFormatInvocation(Node node) { - BaseFormatterImpl baseFormatter = new BaseFormatterImpl(); - Formatter formatter = mock(Formatter.class); - FormatterFactory formatterFactory = mock(FormatterFactory.class, invocation -> formatter); - - Node wrapperNode = mock(Node.class); - doReturn(Collections.singletonList(node)).when(wrapperNode).getChildren(); - doReturn("dummy").when(formatter).format(eq(node), eq(formatterFactory)); - - assertEquals("dummy", baseFormatter.format(wrapperNode, formatterFactory)); - verify(formatter).format(node, formatterFactory); - return formatterFactory; - } - - - private static class BaseFormatterImpl extends BaseFormatter { - - @Override - public String format(Node node, FormatterFactory formatterFactory) { - return childContents(node, formatterFactory); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactoryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactoryTest.java index 7b535f9bf..afef74eb3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactoryTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/MarkdownFormatterFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Michael Clarke + * Copyright (C) 2019-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,84 +18,86 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; -import org.junit.Test; + +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class MarkdownFormatterFactoryTest { +class MarkdownFormatterFactoryTest { @Test - public void testDocumentFormatter() { + void testDocumentFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("Text", testCase.documentFormatter().format(new Document(new Text("Text")), testCase)); + assertEquals("# Heading 1" + System.lineSeparator() + + "Text" + System.lineSeparator() + System.lineSeparator() + + "- List Item 1" + System.lineSeparator() + + "- [Link](url)" + System.lineSeparator() + System.lineSeparator() + + "![alt](url)", testCase.documentFormatter().format(new Document(new Heading(1, new Text("Heading 1")), new Paragraph(new Text("Text")), new List(List.Style.BULLET, new ListItem(new Text("List Item 1")), new ListItem(new Link("url", new Text("Link")))), new Image("alt", "url")))); } @Test - public void testHeadingFormatter() { + void testHeadingFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); assertEquals("## Text" + System.lineSeparator(), - testCase.headingFormatter().format(new Heading(2, new Text("Text")), testCase)); + testCase.headingFormatter().format(new Heading(2, new Text("Text")))); } @Test - public void testImageFormatter() { + void testImageFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("![alt](source)", testCase.imageFormatter().format(new Image("alt", "source"), testCase)); + assertEquals("![alt](source)", testCase.imageFormatter().format(new Image("alt", "source"))); } @Test - public void testLinkFormatter() { + void testLinkFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("[Text](http://url)", testCase.linkFormatter().format(new Link("http://url", new Text("Text")), testCase)); - assertEquals("[http://url](http://url)", testCase.linkFormatter().format(new Link("http://url"), testCase)); + assertEquals("[Text](http://url)", testCase.linkFormatter().format(new Link("http://url", new Text("Text")))); + assertEquals("[http://url](http://url)", testCase.linkFormatter().format(new Link("http://url"))); } @Test - public void testListFormatter() { + void testListFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); assertEquals("- List Item 1" + System.lineSeparator() + System.lineSeparator(), testCase.listFormatter() - .format(new List(List.Style.BULLET, new ListItem(new Text("List Item 1"))), testCase)); + .format(new List(List.Style.BULLET, new ListItem(new Text("List Item 1"))))); } @Test - public void testListFormatterInvalidType() { + void testListFormatterInvalidType() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); + List list = new List(null, new ListItem(new Text("List Item 1"))); + Formatter listFormatter = testCase.listFormatter(); assertThatThrownBy( - () -> testCase.listFormatter().format(new List(null, new ListItem(new Text("List Item 1"))), testCase)) - .isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Unknown list type: null"); + () -> listFormatter.format(list)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown list type: null"); } @Test - public void testListItemFormatter() { + void testListItemFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("Text", testCase.listItemFormatter().format(new ListItem(new Text("Text")), testCase)); + assertEquals("Text", testCase.listItemFormatter().format(new ListItem(new Text("Text")))); } @Test - public void testParagraphFormatter() { + void testParagraphFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); assertEquals("Text" + System.lineSeparator() + System.lineSeparator(), - testCase.paragraphFormatter().format(new Paragraph(new Text("Text")), testCase)); - } - - @Test - public void testTextFormatter() { - MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("Text", testCase.textFormatter().format(new Text("Text"), testCase)); + testCase.paragraphFormatter().format(new Paragraph(new Text("Text")))); } @Test - public void testContentTextFormatterEscapedHtml(){ + void testTextFormatter() { MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("<p> no html allowed", testCase.textFormatter().format(new Text("

no html allowed"), testCase)); - assertEquals("no html <p> allowed", testCase.textFormatter().format(new Text("no html

allowed"), testCase)); - assertEquals("</i>no html <p> allowed<i>", testCase.textFormatter().format(new Text("no html

allowed"), testCase)); + assertEquals("Text", testCase.textFormatter().format(new Text("Text"))); } @Test - public void testContentTextFormatterTrimWhitespaceAtBeginAndEnd(){ + void testContentTextFormatterEscapedHtml(){ MarkdownFormatterFactory testCase = new MarkdownFormatterFactory(); - assertEquals("", testCase.textFormatter().format(new Text(" "), testCase)); + assertEquals("<p> no html allowed ", testCase.textFormatter().format(new Text("

no html allowed "))); + assertEquals("no html <p> allowed", testCase.textFormatter().format(new Text("no html

allowed"))); + assertEquals("</i>no html <p> allowed<i>", testCase.textFormatter().format(new Text("no html

allowed"))); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisIssueSummaryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisIssueSummaryTest.java new file mode 100644 index 000000000..21ad67e84 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisIssueSummaryTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Formatter; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Image; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Link; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +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.verify; +import static org.mockito.Mockito.when; + +class AnalysisIssueSummaryTest { + + @Test + void shouldCreateCorrectOutputDocument() { + AnalysisIssueSummary underTest = AnalysisIssueSummary.builder() + .withProjectKey("projectKey") + .withTypeImageUrl("typeImageUrl") + .withType("type") + .withSeverityImageUrl("severityImageUrl") + .withSeverity("severity") + .withResolution("resolution") + .withMessage("message") + .withIssueUrl("issueUrl") + .withIssueKey("issueKey") + .withEffortInMinutes(101L) + .build(); + + FormatterFactory formatterFactory = mock(FormatterFactory.class); + Formatter documentFormatter = mock(Formatter.class); + when(documentFormatter.format(any())).thenReturn("output content"); + when(formatterFactory.documentFormatter()).thenReturn(documentFormatter); + + assertThat(underTest.format(formatterFactory)).isEqualTo("output content"); + + ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); + verify(documentFormatter).format(documentArgumentCaptor.capture()); + + assertThat(documentArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo( + new Document( + new Paragraph( + new Text("**Type:** type "), + new Image("type", "typeImageUrl") + ), + new Paragraph( + new Text("**Severity:** severity "), + new Image("severity", "severityImageUrl") + ), + new Paragraph(new Text("**Message:** message")), + new Paragraph(new Text("**Duration (min):** 101")), + new Paragraph(new Text("**Resolution:** resolution")), + new Paragraph(new Text("**Project ID:** projectKey **Issue ID:** issueKey")), + new Paragraph(new Link("issueUrl", new Text("View in SonarQube"))) + ) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java new file mode 100644 index 000000000..552bef1c2 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Document; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Formatter; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.FormatterFactory; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Heading; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Image; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Link; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.ListItem; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Paragraph; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.Text; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class AnalysisSummaryTest { + + @Test + void testCreateAnalysisSummary() { + AnalysisSummary underTest = AnalysisSummary.builder() + .withNewDuplications(BigDecimal.valueOf(199)) + .withSummaryImageUrl("summaryImageUrl") + .withProjectKey("projectKey") + .withBugCount(911) + .withBugImageUrl("bugImageUrl") + .withCodeSmellCount(1) + .withCoverage(BigDecimal.valueOf(303)) + .withCodeSmellImageUrl("codeSmellImageUrl") + .withCoverageImageUrl("codeCoverageImageUrl") + .withDashboardUrl("dashboardUrl") + .withDuplications(BigDecimal.valueOf(66)) + .withDuplicationsImageUrl("duplicationsImageUrl") + .withFailedQualityGateConditions(java.util.List.of("issuea", "issueb", "issuec")) + .withNewCoverage(BigDecimal.valueOf(99)) + .withSecurityHotspotCount(69) + .withStatusDescription("status description") + .withStatusImageUrl("statusImageUrl") + .withTotalIssueCount(666) + .withVulnerabilityCount(96) + .withVulnerabilityImageUrl("vulnerabilityImageUrl") + .build(); + + Formatter formatter = mock(Formatter.class); + doReturn("formatted content").when(formatter).format(any()); + FormatterFactory formatterFactory = mock(FormatterFactory.class); + doReturn(formatter).when(formatterFactory).documentFormatter(); + + assertThat(underTest.format(formatterFactory)).isEqualTo("formatted content"); + + ArgumentCaptor documentArgumentCaptor = ArgumentCaptor.forClass(Document.class); + verify(formatter).format(documentArgumentCaptor.capture()); + + Document expectedDocument = new Document(new Paragraph(new Image("status description", "statusImageUrl")), + new List(List.Style.BULLET, + new ListItem(new Text("issuea")), + new ListItem(new Text("issueb")), + new ListItem(new Text("issuec"))), + new Heading(1, new Text("Analysis Details")), + new Heading(2, new Text("666 Issues")), + new List(List.Style.BULLET, + new ListItem( + new Image("Bug","bugImageUrl"), + new Text(" "), + new Text("911 Bugs")), + new ListItem( + new Image("Vulnerability","vulnerabilityImageUrl"), + new Text(" "), + new Text("165 Vulnerabilities")), + new ListItem( + new Image("Code Smell", "codeSmellImageUrl"), + new Text(" "), + new Text("1 Code Smell"))), + new Heading(2, new Text("Coverage and Duplications")), + new List(List.Style.BULLET, + new ListItem( + new Image("Coverage", "codeCoverageImageUrl"), + new Text(" "), + new Text("99.00% Coverage (303.00% Estimated after merge)")), + new ListItem( + new Image("Duplications", "duplicationsImageUrl"), + new Text(" "), + new Text("199.00% Duplicated Code (66.00% Estimated after merge)"))), + new Paragraph(new Text("**Project ID:** projectKey")), + new Paragraph(new Link("dashboardUrl", new Text("View in SonarQube")))); + + assertThat(documentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedDocument); + + } + +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java new file mode 100644 index 000000000..96c90c107 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.report; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.sonar.api.ce.posttask.Project; +import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.config.Configuration; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.platform.Server; +import org.sonar.api.rules.RuleType; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; +import org.sonar.ce.task.projectanalysis.measure.Measure; +import org.sonar.ce.task.projectanalysis.measure.MeasureRepository; +import org.sonar.ce.task.projectanalysis.metric.Metric; +import org.sonar.ce.task.projectanalysis.metric.MetricRepository; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +class ReportGeneratorTest { + + @CsvSource({"12, 0.svg?sanitize=true, 21, 20plus.svg?sanitize=true", + "98, 90.svg?sanitize=true, 1, 3.svg?sanitize=true", + ",NoCoverageInfo.svg?sanitize=true,,NoDuplicationInfo.svg?sanitize=true"}) + @ParameterizedTest + void shouldProduceCorrectAnlysisSummary(String coverage, String coverageImage, String duplications, String duplicationsImage) { + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + doReturn("5").when(analysisDetails).getPullRequestId(); + doReturn("projectKey").when(analysisDetails).getAnalysisProjectKey(); + + TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); + + PostAnalysisIssueVisitor.LightIssue issue1 = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn(Issue.STATUS_CLOSED).when(issue1).status(); + + PostAnalysisIssueVisitor.LightIssue issue2 = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn(Issue.STATUS_OPEN).when(issue2).status(); + doReturn(RuleType.BUG).when(issue2).type(); + + PostAnalysisIssueVisitor.LightIssue issue3 = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn(Issue.STATUS_OPEN).when(issue3).status(); + doReturn(RuleType.SECURITY_HOTSPOT).when(issue3).type(); + + PostAnalysisIssueVisitor.LightIssue issue4 = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn(Issue.STATUS_OPEN).when(issue4).status(); + doReturn(RuleType.CODE_SMELL).when(issue4).type(); + + PostAnalysisIssueVisitor.LightIssue issue5 = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn(Issue.STATUS_OPEN).when(issue5).status(); + doReturn(RuleType.VULNERABILITY).when(issue5).type(); + + PostAnalysisIssueVisitor.LightIssue issue6 = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn(Issue.STATUS_OPEN).when(issue6).status(); + doReturn(RuleType.BUG).when(issue6).type(); + + doReturn(Stream.of(issue1, issue2, issue3, issue4, issue5, issue6).map(i -> { + PostAnalysisIssueVisitor.ComponentIssue componentIssue = + mock(PostAnalysisIssueVisitor.ComponentIssue.class); + doReturn(i).when(componentIssue).getIssue(); + return componentIssue; + }).collect(Collectors.toList())).when(analysisDetails).getIssues(); + + QualityGate.Condition condition1 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.ERROR).when(condition1).getStatus(); + doReturn(CoreMetrics.LINES_TO_COVER.getKey()).when(condition1).getMetricKey(); + doReturn("19").when(condition1).getValue(); + doReturn(QualityGate.Operator.LESS_THAN).when(condition1).getOperator(); + doReturn("20").when(condition1).getErrorThreshold(); + + QualityGate.Condition condition2 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.ERROR).when(condition2).getStatus(); + doReturn(CoreMetrics.CODE_SMELLS.getKey()).when(condition2).getMetricKey(); + doReturn("2").when(condition2).getValue(); + doReturn(QualityGate.Operator.GREATER_THAN).when(condition2).getOperator(); + doReturn("0").when(condition2).getErrorThreshold(); + + QualityGate.Condition condition3 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.ERROR).when(condition3).getStatus(); + doReturn(CoreMetrics.LINE_COVERAGE.getKey()).when(condition3).getMetricKey(); + doReturn("68").when(condition3).getValue(); + doReturn(QualityGate.Operator.LESS_THAN).when(condition3).getOperator(); + doReturn("80").when(condition3).getErrorThreshold(); + + QualityGate.Condition condition4 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.ERROR).when(condition4).getStatus(); + doReturn(CoreMetrics.NEW_SECURITY_RATING.getKey()).when(condition4).getMetricKey(); + doReturn("5").when(condition4).getValue(); + doReturn(QualityGate.Operator.GREATER_THAN).when(condition4).getOperator(); + doReturn("4").when(condition4).getErrorThreshold(); + + QualityGate.Condition condition5 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.ERROR).when(condition5).getStatus(); + doReturn(CoreMetrics.RELIABILITY_RATING.getKey()).when(condition5).getMetricKey(); + doReturn("1").when(condition5).getValue(); + doReturn(QualityGate.Operator.LESS_THAN).when(condition5).getOperator(); + doReturn("3").when(condition5).getErrorThreshold(); + + QualityGate.Condition condition6 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.ERROR).when(condition6).getStatus(); + doReturn(CoreMetrics.NEW_COVERAGE.getKey()).when(condition6).getMetricKey(); + doReturn(coverage).when(condition6).getValue(); + doReturn(QualityGate.Operator.GREATER_THAN).when(condition6).getOperator(); + doReturn("15").when(condition6).getErrorThreshold(); + + QualityGate.Condition condition7 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.OK).when(condition7).getStatus(); + doReturn(CoreMetrics.NEW_BUGS.getKey()).when(condition7).getMetricKey(); + doReturn("0").when(condition7).getValue(); + doReturn(QualityGate.Operator.LESS_THAN).when(condition7).getOperator(); + doReturn("1").when(condition7).getErrorThreshold(); + + QualityGate.Condition condition8 = mock(QualityGate.Condition.class); + doReturn(QualityGate.EvaluationStatus.OK).when(condition8).getStatus(); + doReturn(CoreMetrics.NEW_DUPLICATED_LINES_DENSITY.getKey()).when(condition8).getMetricKey(); + doReturn(duplications).when(condition8).getValue(); + doReturn(QualityGate.Operator.GREATER_THAN).when(condition8).getOperator(); + doReturn("1").when(condition8).getErrorThreshold(); + + + doReturn(Arrays.asList(condition1, condition2, condition3, condition4)) + .when(analysisDetails).findFailedQualityGateConditions(); + doAnswer(i -> Stream.of(condition1, condition2, condition3, condition4, condition5, condition6, condition7, condition8).filter(condition -> condition.getMetricKey().equals(i.getArgument(0, String.class))).findFirst()).when(analysisDetails).findQualityGateCondition(any()); + doReturn(QualityGate.Status.ERROR).when(analysisDetails).getQualityGateStatus(); + + Project project = mock(Project.class); + doReturn("Project Key").when(project).getKey(); + + Component rootComponent = mock(Component.class); + doReturn(rootComponent).when(treeRootHolder).getRoot(); + + MeasureRepository measureRepository = mock(MeasureRepository.class); + if (coverage != null) { + doReturn(Optional.of(Measure.newMeasureBuilder().create(Double.parseDouble(coverage), 2, "data")), + Optional.of(Measure.newMeasureBuilder().create(Double.parseDouble(duplications), 2, "data"))).when(measureRepository) + .getRawMeasure(eq(rootComponent), any(Metric.class)); + } + + MetricRepository metricRepository = mock(MetricRepository.class); + doReturn(mock(Metric.class)).when(metricRepository).getByKey(anyString()); + + Server server = mock(Server.class); + doReturn("http://localhost:9000").when(server).getPublicRootUrl(); + Configuration configuration = mock(Configuration.class); + ReportGenerator underTest = new ReportGenerator(server, configuration, measureRepository, metricRepository, treeRootHolder); + + AnalysisSummary expected = AnalysisSummary.builder() + .withBugCount(2) + .withBugImageUrl("http://localhost:9000/static/communityBranchPlugin/common/bug.svg?sanitize=true") + .withCoverage(null == coverage ? null : new BigDecimal(coverage)) + .withCoverageImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/CoverageChart/" + coverageImage) + .withNewCoverage(null == coverage ? null : new BigDecimal(coverage)) + .withDuplications(null == duplications ? null : new BigDecimal(duplications).setScale(1, RoundingMode.CEILING)) + .withDuplicationsImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/Duplications/" + duplicationsImage) + .withNewDuplications(null == duplications ? null : new BigDecimal(duplications)) + .withCodeSmellCount(1) + .withCodeSmellImageUrl("http://localhost:9000/static/communityBranchPlugin/common/code_smell.svg?sanitize=true") + .withDashboardUrl("http://localhost:9000/dashboard?id=projectKey&pullRequest=5") + .withProjectKey("projectKey") + .withSummaryImageUrl("http://localhost:9000/static/communityBranchPlugin/common/icon.png") + .withSecurityHotspotCount(1) + .withVulnerabilityCount(1) + .withVulnerabilityImageUrl("http://localhost:9000/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true") + .withStatusDescription("Failed") + .withStatusImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true") + .withTotalIssueCount(5) + .withFailedQualityGateConditions(List.of("19 Lines to Cover (is less than 20)", + "2 Code Smells (is greater than 0)", + "68.00% Line Coverage (is less than 80.00%)", + "E Security Rating on New Code (is worse than D)")) + .build(); + + assertThat(underTest.createAnalysisSummary(analysisDetails)) + .usingRecursiveComparison() + .isEqualTo(expected); + } + + @CsvSource({"SECURITY_HOTSPOT, security_hotspots?id=project-key&pullRequest=pull-request-id&hotspots=issue-key", + "BUG, project/issues?id=project-key&pullRequest=pull-request-id&issues=issue-key&open=issue-key"}) + @ParameterizedTest + void shouldProduceCorrectAnalysisIssueSummary(RuleType ruleType, String issueUrlPostfix) { + MeasureRepository measureRepository = mock(MeasureRepository.class); + MetricRepository metricRepository = mock(MetricRepository.class); + TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); + + Server server = mock(Server.class); + doReturn("http://target.host:port/path/to/root").when(server).getPublicRootUrl(); + Configuration configuration = mock(Configuration.class); + ReportGenerator underTest = new ReportGenerator(server, configuration, measureRepository, metricRepository, treeRootHolder); + + AnalysisIssueSummary expected = AnalysisIssueSummary.builder() + .withEffortInMinutes(101L) + .withIssueKey("issue-key") + .withIssueUrl("http://target.host:port/path/to/root/" + issueUrlPostfix) + .withMessage("message") + .withResolution("FIXED") + .withSeverity("CRITICAL") + .withSeverityImageUrl("http://target.host:port/path/to/root/static/communityBranchPlugin/checks/Severity/critical.svg?sanitize=true") + .withType(ruleType.name()) + .withTypeImageUrl("http://target.host:port/path/to/root/static/communityBranchPlugin/checks/IssueType/" + ruleType.name().toLowerCase(Locale.ENGLISH) + ".svg?sanitize=true") + .withProjectKey("project-key") + .build(); + + PostAnalysisIssueVisitor.LightIssue lightIssue = mock(PostAnalysisIssueVisitor.LightIssue.class); + doReturn("issue-key").when(lightIssue).key(); + doReturn("CRITICAL").when(lightIssue).severity(); + doReturn("message").when(lightIssue).getMessage(); + doReturn("FIXED").when(lightIssue).resolution(); + doReturn(ruleType).when(lightIssue).type(); + doReturn(101L).when(lightIssue).effortInMinutes(); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + doReturn(lightIssue).when(componentIssue).getIssue(); + + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + doReturn("project-key").when(analysisDetails).getAnalysisProjectKey(); + doReturn("pull-request-id").when(analysisDetails).getPullRequestId(); + + assertThat(underTest.createAnalysisIssueSummary(componentIssue, analysisDetails)) + .usingRecursiveComparison() + .isEqualTo(expected); + } + +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationFactoryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationFactoryTest.java new file mode 100644 index 000000000..5d4ebc490 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/BranchConfigurationFactoryTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner; + +import org.junit.jupiter.api.Test; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.BranchInfo; +import org.sonar.scanner.scan.branch.BranchType; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.Mockito.mock; +import static org.mockito.Mockito.when; + +class BranchConfigurationFactoryTest { + + @Test + void shouldReturnBranchWithNoTargetIfNoProjectBranchesExist() { + ProjectBranches projectBranches = mock(ProjectBranches.class); + when(projectBranches.isEmpty()).thenReturn(true); + + BranchConfigurationFactory underTest = new BranchConfigurationFactory(); + BranchConfiguration actual = underTest.createBranchConfiguration("branch", projectBranches); + + assertThat(actual).usingRecursiveComparison().isEqualTo(new CommunityBranchConfiguration("branch", BranchType.BRANCH, null, null, null)); + } + + @Test + void shouldReturnBranchWithDefaultReferenceIfSpecifiedBranchDoesNotExist() { + ProjectBranches projectBranches = mock(ProjectBranches.class); + when(projectBranches.isEmpty()).thenReturn(false); + when(projectBranches.defaultBranchName()).thenReturn("default"); + when(projectBranches.get(any())).thenReturn(null); + + BranchConfigurationFactory underTest = new BranchConfigurationFactory(); + BranchConfiguration actual = underTest.createBranchConfiguration("branch", projectBranches); + + assertThat(actual).usingRecursiveComparison().isEqualTo(new CommunityBranchConfiguration("branch", BranchType.BRANCH, "default", null, null)); + } + + @Test + void shouldReturnBranchWithSelfReferenceIfSpecifiedBranchDoesExist() { + ProjectBranches projectBranches = mock(ProjectBranches.class); + when(projectBranches.isEmpty()).thenReturn(false); + when(projectBranches.defaultBranchName()).thenReturn("default"); + when(projectBranches.get(any())).thenReturn(mock(BranchInfo.class)); + + BranchConfigurationFactory underTest = new BranchConfigurationFactory(); + BranchConfiguration actual = underTest.createBranchConfiguration("branch", projectBranches); + + assertThat(actual).usingRecursiveComparison().isEqualTo(new CommunityBranchConfiguration("branch", BranchType.BRANCH, "branch", null, null)); + } + + @Test + void shouldReturnPullRequestWithNoTargetIfNoProjectBranchesExist() { + ProjectBranches projectBranches = mock(ProjectBranches.class); + when(projectBranches.isEmpty()).thenReturn(true); + + BranchConfigurationFactory underTest = new BranchConfigurationFactory(); + BranchConfiguration actual = underTest.createPullRequestConfiguration("key", "source", "target", projectBranches); + + assertThat(actual).usingRecursiveComparison().isEqualTo(new CommunityBranchConfiguration("source", BranchType.PULL_REQUEST, null, "target", "key")); + } + + @Test + void shouldReturnPullRequestWithTargetOfTargetAsReferenceIfTargetBranchExists() { + ProjectBranches projectBranches = mock(ProjectBranches.class); + when(projectBranches.isEmpty()).thenReturn(false); + BranchInfo branchInfo = new BranchInfo("target", BranchType.PULL_REQUEST, false, "target2"); + when(projectBranches.get("target")).thenReturn(branchInfo); + BranchInfo branchInfo2 = new BranchInfo("target2", BranchType.BRANCH, false, "target3"); + when(projectBranches.get("target2")).thenReturn(branchInfo2); + + BranchConfigurationFactory underTest = new BranchConfigurationFactory(); + BranchConfiguration actual = underTest.createPullRequestConfiguration("key", "source", "target", projectBranches); + + assertThat(actual).usingRecursiveComparison().isEqualTo(new CommunityBranchConfiguration("source", BranchType.PULL_REQUEST, "target2", "target", "key")); + } + + @Test + void shouldThrowExceptionIfPullRequestTargetsOtherPullRequestWithoutATarget() { + ProjectBranches projectBranches = mock(ProjectBranches.class); + when(projectBranches.isEmpty()).thenReturn(false); + BranchInfo branchInfo = new BranchInfo("target", BranchType.PULL_REQUEST, false, null); + when(projectBranches.get("target")).thenReturn(branchInfo); + + BranchConfigurationFactory underTest = new BranchConfigurationFactory(); + assertThatThrownBy(() -> underTest.createPullRequestConfiguration("key", "source", "target", projectBranches)).hasMessage("The branch 'target' of type PULL_REQUEST does not have a target"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java index b3927217f..7312eb828 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/CommunityBranchConfigurationLoaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,328 +18,106 @@ */ package com.github.mc1arke.sonarqube.plugin.scanner; -import org.hamcrest.core.IsEqual; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Test; import org.sonar.api.notifications.AnalysisWarnings; import org.sonar.api.utils.System2; import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonar.scanner.scan.branch.BranchConfigurationLoader; -import org.sonar.scanner.scan.branch.BranchInfo; -import org.sonar.scanner.scan.branch.BranchType; import org.sonar.scanner.scan.branch.DefaultBranchConfiguration; import org.sonar.scanner.scan.branch.ProjectBranches; import org.sonar.scanner.scan.branch.ProjectPullRequests; -import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; +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.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** * @author Michael Clarke */ -public class CommunityBranchConfigurationLoaderTest { - - private final ExpectedException expectedException = ExpectedException.none(); +class CommunityBranchConfigurationLoaderTest { private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); private final System2 system2 = mock(System2.class); - private final BranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(system2, analysisWarnings); - - @Rule - public ExpectedException expectedException() { - return expectedException; - } - - @Test - public void testExceptionWhenNoExistingBranchAndBranchParamsPresent() { - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "dummy"); - - BranchConfiguration branchConfiguration = testCase.load(parameters, branchInfo, mock(ProjectPullRequests.class)); - - assertEquals("dummy", branchConfiguration.branchName()); - assertNull(branchConfiguration.referenceBranchName()); - assertEquals(BranchType.BRANCH, branchConfiguration.branchType()); - } - - @Test - public void testDefaultConfigWhenNoExistingBranchAndBranchNameParamMaster() { - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "master"); - - BranchConfiguration branchConfiguration = testCase.load(parameters, mock(ProjectBranches.class), mock(ProjectPullRequests.class)); - assertEquals("master", branchConfiguration.branchName()); - assertNull(branchConfiguration.targetBranchName()); - assertNull(branchConfiguration.referenceBranchName()); - assertEquals(BranchType.BRANCH, branchConfiguration.branchType()); } - - @Test - public void testErrorWhenNoExistingBranchAndBranchTargetMasterButNoSourceBranch() { - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "dummy"); - - BranchConfiguration branchConfiguration = testCase.load(parameters, branchInfo, mock(ProjectPullRequests.class)); - - assertEquals("dummy", branchConfiguration.branchName()); - assertNull(branchConfiguration.referenceBranchName()); - assertNull(branchConfiguration.targetBranchName()); - assertEquals(BranchType.BRANCH, branchConfiguration.branchType()); - } + private final BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + private final BranchAutoConfigurer branchAutoConfigurer = mock(BranchAutoConfigurer.class); + private final BranchConfigurationLoader testCase = new CommunityBranchConfigurationLoader(system2, analysisWarnings, branchConfigurationFactory, List.of(branchAutoConfigurer)); @Test - public void testWarningWhenTargetBranchParameterSpecified() { - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - parameters.put("sonar.branch.target", "dummy"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); + void shouldReturnResultFromAutoConfigurerIfPresentAndNoParametersSpecified() { + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchAutoConfigurer.detectConfiguration(any(), any())).thenReturn(Optional.of(branchConfiguration)); ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("masterxxx")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("masterxxx"); - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); + BranchConfiguration actual = testCase.load(Map.of(), projectBranches, mock(ProjectPullRequests.class)); - assertNull(result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("masterxxx", result.referenceBranchName()); - assertFalse(result.isPullRequest()); - - verify(analysisWarnings).addUnique(eq("Property 'sonar.branch.target' is no longer supported")); - } - - - @Test - public void testDefaultConfigWhenNoExistingBranchAndBranchParamsAllMaster() { - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "master"); - - BranchConfiguration branchConfiguration = testCase.load(parameters, branchInfo, mock(ProjectPullRequests.class)); - - assertEquals("master", branchConfiguration.branchName()); - assertEquals(BranchType.BRANCH, branchConfiguration.branchType()); - assertNull(branchConfiguration.referenceBranchName()); - assertNull(branchConfiguration.targetBranchName()); - } - - @Test - public void testDefaultBranchInfoWhenNoBranchParametersSpecifiedAndNoBranchesExist() { - ProjectBranches branchInfo = mock(ProjectBranches.class); - when(branchInfo.isEmpty()).thenReturn(true); - - Map parameters = new HashMap<>(); - parameters.put("dummy", "dummy"); - - - assertEquals(DefaultBranchConfiguration.class, - testCase.load(parameters, branchInfo, mock(ProjectPullRequests.class)).getClass()); - } - - @Test - public void testDefaultBranchInfoWhenNoParametersSpecified() { - assertEquals(DefaultBranchConfiguration.class, testCase.load(new HashMap<>(), mock(ProjectBranches.class), - mock(ProjectPullRequests.class)).getClass()); + assertThat(actual).isSameAs(branchConfiguration); + verify(branchAutoConfigurer).detectConfiguration(system2, projectBranches); + verifyNoInteractions(branchConfigurationFactory); } @Test - public void testValidBranchInfoWhenAllBranchParametersSpecified() { - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedFeatureBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("masterBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); + void shouldReturnDefaultBranchIfAutoConfigurerNoResultAndNoParametersSpecified() { + when(branchAutoConfigurer.detectConfiguration(any(), any())).thenReturn(Optional.empty()); ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertNull(result.targetBranchName()); - assertEquals("feature/shortLivedFeatureBranch", result.branchName()); - assertEquals("master", result.referenceBranchName()); - assertFalse(result.isPullRequest()); - expectedException - .expectMessage(IsEqual.equalTo("Only a branch of type PULL_REQUEST can have a Pull Request key")); - expectedException.expect(IllegalStateException.class); + BranchConfiguration actual = testCase.load(Map.of(), projectBranches, mock(ProjectPullRequests.class)); - result.pullRequestKey(); + assertThat(actual).usingRecursiveComparison().isEqualTo(new DefaultBranchConfiguration()); + verify(branchAutoConfigurer).detectConfiguration(system2, projectBranches); + verifyNoInteractions(branchConfigurationFactory); } @Test - public void testValidBranchInfoWhenOnlySourceBranchSpecifiedAndMasterExists() { - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); - + void shouldCreateBranchConfigurationIfAnyBranchPropertiesSet() { ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("masterxxx")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("masterxxx"); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); + BranchConfiguration actual = testCase.load(Map.of("sonar.branch.name", "branch", "sonar.branch.target", "target"), projectBranches, mock(ProjectPullRequests.class)); - assertNull(result.targetBranchName()); - assertEquals("feature/shortLivedBranch", result.branchName()); - assertEquals("masterxxx", result.referenceBranchName()); - assertFalse(result.isPullRequest()); + assertThat(actual).isSameAs(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + verifyNoInteractions(branchAutoConfigurer); + verify(analysisWarnings).addUnique("Property 'sonar.branch.target' is no longer supported"); } @Test - public void testExceptionWhenOnlySourceBranchSpecifiedAndNoMasterExists() { - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "feature/shortLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("defaultBranchInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); - + void shouldCreatePullConfigurationIfAnyPullRequestPropertiesSet() { ProjectBranches projectBranches = mock(ProjectBranches.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); - BranchConfiguration branchConfiguration = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); + BranchConfiguration actual = testCase.load(Map.of("sonar.pullrequest.key", "key", "sonar.pullrequest.branch", "source", "sonar.pullrequest.base", "target"), projectBranches, mock(ProjectPullRequests.class)); - assertEquals("feature/shortLivedBranch", branchConfiguration.branchName()); - assertNull(branchConfiguration.referenceBranchName()); - assertNull(branchConfiguration.targetBranchName()); - assertEquals(BranchType.BRANCH, branchConfiguration.branchType()); + assertThat(actual).isSameAs(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("key", "source", "target", projectBranches); + verifyNoInteractions(branchAutoConfigurer); + verifyNoInteractions(analysisWarnings); } @Test - public void testExistingBranchOnlySourceParameters() { - Map parameters = new HashMap<>(); - parameters.put("sonar.branch.name", "longLivedBranch"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("longLivedBranch"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("longLivedBranch")).thenReturn(mockTargetBranchInfo); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertNull(result.targetBranchName()); - assertEquals("longLivedBranch", result.branchName()); - assertEquals("longLivedBranch", result.referenceBranchName()); - assertFalse(result.isPullRequest()); + void shouldThrowErrorIfBothBranchAndPullRequestParametersPresent() { + assertThatThrownBy(() -> testCase.load(Map.of("sonar.pullrequest.key", "key", "sonar.pullrequest.branch", "source", "sonar.branch.name", "branch"), mock(ProjectBranches.class), mock(ProjectPullRequests.class))).hasMessage("sonar.pullrequest and sonar.branch parameters should not be specified in the same scan"); } @Test - public void testPullRequestAllParameters() { - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.base", "target"); - parameters.put("sonar.pullrequest.key", "pr-key"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("targetInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("target")).thenReturn(mockTargetBranchInfo); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("target", result.targetBranchName()); - assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("target", result.referenceBranchName()); - assertTrue(result.isPullRequest()); - assertEquals("pr-key", result.pullRequestKey()); + void shouldThrowErrorIfPullRequestAnlysisWithoutPullRequestKey() { + assertThatThrownBy(() -> testCase.load(Map.of("sonar.pullrequest.base", "target"), mock(ProjectBranches.class), mock(ProjectPullRequests.class))).hasMessage("sonar.pullrequest.key is required for a pull request analysis"); } - @Test - public void testPullRequestMandatoryParameters() { - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.key", "pr-key"); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("masterInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("master", result.referenceBranchName()); - assertTrue(result.isPullRequest()); - } - - @Test - public void testPullRequestMandatoryParameters2() { - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.key", "pr-key"); - parameters.put("sonar.pullrequest.base", ""); - - BranchInfo mockTargetBranchInfo = mock(BranchInfo.class); - when(mockTargetBranchInfo.name()).thenReturn("masterInfo"); - when(mockTargetBranchInfo.type()).thenReturn(BranchType.BRANCH); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - when(projectBranches.get("master")).thenReturn(mockTargetBranchInfo); - when(projectBranches.defaultBranchName()).thenReturn("master"); - - BranchConfiguration result = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - - assertEquals("master", result.targetBranchName()); - assertEquals("feature/sourceBranch", result.branchName()); - assertEquals("master", result.referenceBranchName()); - assertTrue(result.isPullRequest()); - } - - - @Test - public void testPullRequestNoSuchTarget() { - Map parameters = new HashMap<>(); - parameters.put("sonar.pullrequest.branch", "feature/sourceBranch"); - parameters.put("sonar.pullrequest.base", "missingTarget"); - parameters.put("sonar.pullrequest.key", "pr-key"); - - - ProjectBranches projectBranches = mock(ProjectBranches.class); - - BranchConfiguration branchConfiguration = testCase.load(parameters, projectBranches, mock(ProjectPullRequests.class)); - assertEquals("feature/sourceBranch", branchConfiguration.branchName()); - assertEquals("missingTarget", branchConfiguration.targetBranchName()); - assertNull(branchConfiguration.referenceBranchName()); - assertEquals(BranchType.PULL_REQUEST, branchConfiguration.branchType()); + void shouldThrowErrorIfPullRequestAnalysisWithoutPullRequestBranch() { + assertThatThrownBy(() -> testCase.load(Map.of("sonar.pullrequest.key", "key"), mock(ProjectBranches.class), mock(ProjectPullRequests.class))).hasMessage("sonar.pullrequest.branch is required for a pull request analysis"); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/AzureDevopsAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/AzureDevopsAutoConfigurerTest.java new file mode 100644 index 000000000..9a7577aeb --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/AzureDevopsAutoConfigurerTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class AzureDevopsAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotTfBuild() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + AzureDevopsAutoConfigurer underTest = new AzureDevopsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnOptionalEmptyIfTfBuildWithNoPullRequestId() { + System2 system2 = mock(System2.class); + when(system2.envVariable("TF_BUILD")).thenReturn("true"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + AzureDevopsAutoConfigurer underTest = new AzureDevopsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnConfigurationBasedOnAllEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("TF_BUILD")).thenReturn("true"); + when(system2.envVariable("SYSTEM_PULLREQUEST_PULLREQUESTID")).thenReturn("id"); + when(system2.envVariable("SYSTEM_PULLREQUEST_SOURCEBRANCH")).thenReturn("source"); + when(system2.envVariable("SYSTEM_PULLREQUEST_TARGETBRANCH")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + AzureDevopsAutoConfigurer underTest = new AzureDevopsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/BitbucketPipelinesAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/BitbucketPipelinesAutoConfigurerTest.java new file mode 100644 index 000000000..ee0710762 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/BitbucketPipelinesAutoConfigurerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class BitbucketPipelinesAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotCi() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + BitbucketPipelinesAutoConfigurer underTest = new BitbucketPipelinesAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnOptionalEmptyIfCiWithNoBranchProperty() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CI")).thenReturn("true"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + BitbucketPipelinesAutoConfigurer underTest = new BitbucketPipelinesAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnBranchConfigurationBasedOnNoPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CI")).thenReturn("true"); + when(system2.envVariable("BITBUCKET_BRANCH")).thenReturn("branch"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + BitbucketPipelinesAutoConfigurer underTest = new BitbucketPipelinesAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + } + + @Test + void shouldReturnPullRequestConfigurationBasedOnPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CI")).thenReturn("true"); + when(system2.envVariable("BITBUCKET_BRANCH")).thenReturn("source"); + when(system2.envVariable("BITBUCKET_PR_ID")).thenReturn("id"); + when(system2.envVariable("BITBUCKET_PR_DESTINATION_BRANCH")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + BitbucketPipelinesAutoConfigurer underTest = new BitbucketPipelinesAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CirrusCiAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CirrusCiAutoConfigurerTest.java new file mode 100644 index 000000000..38f326ce4 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CirrusCiAutoConfigurerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class CirrusCiAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotCirrusCi() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CirrusCiAutoConfigurer underTest = new CirrusCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnOptionalEmptyIfCirrusCiWithNoBranchProperty() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CIRRUS_CI")).thenReturn("true"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CirrusCiAutoConfigurer underTest = new CirrusCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnBranchConfigurationBasedOnNoPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CIRRUS_CI")).thenReturn("true"); + when(system2.envVariable("CIRRUS_BRANCH")).thenReturn("branch"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CirrusCiAutoConfigurer underTest = new CirrusCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + } + + @Test + void shouldReturnPullRequestConfigurationBasedOnPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CIRRUS_CI")).thenReturn("true"); + when(system2.envVariable("CIRRUS_BRANCH")).thenReturn("source"); + when(system2.envVariable("CIRRUS_PR")).thenReturn("id"); + when(system2.envVariable("CIRRUS_BASE_BRANCH")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CirrusCiAutoConfigurer underTest = new CirrusCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CodeMagicAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CodeMagicAutoConfigurerTest.java new file mode 100644 index 000000000..8a99fcc23 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/CodeMagicAutoConfigurerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class CodeMagicAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotCi() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CodeMagicAutoConfigurer underTest = new CodeMagicAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnOptionalEmptyIfCiWithNoFciBranchProperty() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CI")).thenReturn("true"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CodeMagicAutoConfigurer underTest = new CodeMagicAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnBranchConfigurationBasedOnNoPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CI")).thenReturn("true"); + when(system2.envVariable("FCI_BRANCH")).thenReturn("branch"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CodeMagicAutoConfigurer underTest = new CodeMagicAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + } + + @Test + void shouldReturnPullRequestConfigurationBasedOnPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("CI")).thenReturn("true"); + when(system2.envVariable("FCI_BRANCH")).thenReturn("source"); + when(system2.envVariable("FCI_PULL_REQUEST")).thenReturn("true"); + when(system2.envVariable("FCI_PULL_REQUEST_NUMBER")).thenReturn("id"); + when(system2.envVariable("FCI_PULL_REQUEST_DEST")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + CodeMagicAutoConfigurer underTest = new CodeMagicAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GithubActionsAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GithubActionsAutoConfigurerTest.java new file mode 100644 index 000000000..54b04d6e1 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GithubActionsAutoConfigurerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class GithubActionsAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotGithubActions() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GithubActionsAutoConfigurer underTest = new GithubActionsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnOptionalEmptyIfGithubActionsWithNoGithubRefProperty() { + System2 system2 = mock(System2.class); + when(system2.envVariable("GITHUB_ACTIONS")).thenReturn("true"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GithubActionsAutoConfigurer underTest = new GithubActionsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnBranchConfigurationBasedOnNoPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("GITHUB_ACTIONS")).thenReturn("true"); + when(system2.envVariable("GITHUB_REF")).thenReturn("refs/heads/branch"); + when(system2.envVariable("GITHUB_REF_NAME")).thenReturn("branch"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GithubActionsAutoConfigurer underTest = new GithubActionsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + } + + @Test + void shouldReturnPullRequestConfigurationBasedOnPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("GITHUB_ACTIONS")).thenReturn("true"); + when(system2.envVariable("GITHUB_HEAD_REF")).thenReturn("source"); + when(system2.envVariable("GITHUB_REF")).thenReturn("refs/pull/id/merge"); + when(system2.envVariable("GITHUB_BASE_REF")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GithubActionsAutoConfigurer underTest = new GithubActionsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GitlabCiAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GitlabCiAutoConfigurerTest.java new file mode 100644 index 000000000..4fa565255 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/GitlabCiAutoConfigurerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class GitlabCiAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotGitlabCi() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GitlabCiAutoConfigurer underTest = new GitlabCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnBranchConfigurationBasedOnNoPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("GITLAB_CI")).thenReturn("true"); + when(system2.envVariable("CI_COMMIT_REF_NAME")).thenReturn("branch"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GitlabCiAutoConfigurer underTest = new GitlabCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + } + + @Test + void shouldReturnPullRequestConfigurationBasedOnPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("GITLAB_CI")).thenReturn("true"); + when(system2.envVariable("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")).thenReturn("source"); + when(system2.envVariable("CI_MERGE_REQUEST_IID")).thenReturn("id"); + when(system2.envVariable("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + GitlabCiAutoConfigurer underTest = new GitlabCiAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/JenkinsAutoConfigurerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/JenkinsAutoConfigurerTest.java new file mode 100644 index 000000000..b30a70244 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/scanner/autoconfiguration/JenkinsAutoConfigurerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration; + +import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.sonar.api.utils.System2; +import org.sonar.scanner.scan.branch.BranchConfiguration; +import org.sonar.scanner.scan.branch.ProjectBranches; + +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.verify; +import static org.mockito.Mockito.when; + +class JenkinsAutoConfigurerTest { + + @Test + void shouldReturnOptionalEmptyIfNotJenkins() { + System2 system2 = mock(System2.class); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + JenkinsAutoConfigurer underTest = new JenkinsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).isEmpty(); + } + + @Test + void shouldReturnBranchConfigurationBasedOnNoPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("JENKINS_HOME")).thenReturn("/path/to/home"); + when(system2.envVariable("BRANCH_NAME")).thenReturn("branch"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createBranchConfiguration(any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + JenkinsAutoConfigurer underTest = new JenkinsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createBranchConfiguration("branch", projectBranches); + } + + @Test + void shouldReturnPullRequestConfigurationBasedOnPrIdInEnvironmentParameters() { + System2 system2 = mock(System2.class); + when(system2.envVariable("JENKINS_HOME")).thenReturn("/path/to/home"); + when(system2.envVariable("CHANGE_BRANCH")).thenReturn("source"); + when(system2.envVariable("CHANGE_ID")).thenReturn("id"); + when(system2.envVariable("CHANGE_TARGET")).thenReturn("target"); + BranchConfigurationFactory branchConfigurationFactory = mock(BranchConfigurationFactory.class); + BranchConfiguration branchConfiguration = mock(BranchConfiguration.class); + when(branchConfigurationFactory.createPullRequestConfiguration(any(), any(), any(), any())).thenReturn(branchConfiguration); + ProjectBranches projectBranches = mock(ProjectBranches.class); + + JenkinsAutoConfigurer underTest = new JenkinsAutoConfigurer(branchConfigurationFactory); + assertThat(underTest.detectConfiguration(system2, projectBranches)).contains(branchConfiguration); + verify(branchConfigurationFactory).createPullRequestConfiguration("id", "source", "target", projectBranches); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidatorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidatorTest.java index 3afcaccee..9acba694c 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidatorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/validator/BitbucketValidatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Michael Clarke + * Copyright (C) 2021-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -72,7 +72,7 @@ void testInvalidConfigurationExceptionRethrownIfCreateClientThrows() { void testInvalidConfigurationExceptionThrownIfRetrieveRepositoryFails() throws IOException { BitbucketValidator underTest = new BitbucketValidator(bitbucketClientFactory); BitbucketClient bitbucketClient = mock(BitbucketClient.class); - when(bitbucketClient.retrieveRepository(any(), any())).thenThrow(new IOException("dummy")); + when(bitbucketClient.retrieveRepository()).thenThrow(new IOException("dummy")); when(bitbucketClientFactory.createClient(any(), any())).thenReturn(bitbucketClient); assertThatThrownBy(() -> underTest.validate(projectAlmSettingDto, almSettingDto)) .isInstanceOf(InvalidConfigurationException.class)