diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..6cbd86e12 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @jenkinsci/github-plugin-developers diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..dbae4a465 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: maven + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: master + labels: + - dependencies diff --git a/.github/workflows/jenkins-security-scan.yml b/.github/workflows/jenkins-security-scan.yml new file mode 100644 index 000000000..c7b41fc29 --- /dev/null +++ b/.github/workflows/jenkins-security-scan.yml @@ -0,0 +1,21 @@ +name: Jenkins Security Scan + +on: + push: + branches: + - master + pull_request: + types: [ opened, synchronize, reopened ] + workflow_dispatch: + +permissions: + security-events: write + contents: read + actions: read + +jobs: + security-scan: + uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 + with: + java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. + # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 000000000..1f8a181b6 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,17 @@ +# Note: additional setup is required, see https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc + +name: Release Drafter + +on: + push: + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into the default branch + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 99b3c61f0..41dfd3e40 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target # autogenerated resources src/main/webapp/css/* +.vscode/ diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 43d628161..4e0774d51 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,6 +2,6 @@ io.jenkins.tools.incrementals git-changelist-maven-extension - 1.2 + 1.8 diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index fa4f7b499..000000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,110 +0,0 @@ -/* -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you under the Apache License, Version 2.0 (the -"License"); you may not use this file except in compliance -with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. -*/ - -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = - "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: : " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100755 index 01e679973..000000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 00d32aab1..d58dfb70b 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,19 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip \ No newline at end of file +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4ecd635f..82d635c21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,16 @@ # Functional contribution -We are welcome for any contribution. But every new feature implemented in this plugin should: +We welcome any contribution. But every new feature implemented in this plugin should: -- Be useful enough for lot of people (should not cover only your professional case). -- Should not break existing use cases and should avoid breaking the backward compatibility in existing APIs. - - If the compatibility break is required, it should be well justified. +- Be useful enough for many people (should cover more than just your professional case). +- Should not break existing use cases and should avoid breaking backward compatibility in existing APIs. + - If a compatibility break is required, it should be well justified. [Guide](https://wiki.eclipse.org/Evolving_Java-based_APIs_2) - and [jenkins solutions](https://wiki.jenkins-ci.org/display/JENKINS/Hint+on+retaining+backward+compatibility) can help to retain the backward compatibility. + and [jenkins solutions](https://wiki.jenkins-ci.org/display/JENKINS/Hint+on+retaining+backward+compatibility) can help to retain backward compatibility. - Should be easily maintained (so maintainers need some time to think about architecture of implementation). - Have at least one test for positive use case. -This plugin is used by lot of people, so it should be stable enough. Please ensure your change is compatible at least with the last LTS line. +This plugin is used by many people, so it should be stable. Please ensure your change is compatible at least with the last LTS line. Any core dependency upgrade must be justified. # Code Style Guidelines @@ -20,9 +20,9 @@ Checkstyle rules are more important than this document. ## Resulting from long experience -* To the largest extent possible, all fields shall be private. Use an IDE to generate the getters and setters. -* If a class has more than one `volatile` member field, it is probable that there are subtle race conditions. Please consider where appropriate encapsulation of the multiple fields into an immutable value object replace the multiple `volatile` member fields with a single `volatile` reference to the value object (or perhaps better yet an `AtomicReference` to allow for `compareAndSet` - if compare-and-set logic is appropriate). -* If it is `Serializable` it shall have a `serialVersionUID` field. Unless code has shipped to users, the initial value of the `serialVersionUID` field shall be `1L`. +- To the largest extent possible, all fields should be private. Use an IDE to generate the getters and setters. +- If a class has more than one `volatile` member field, it is probable that there are subtle race conditions. Please consider, where appropriate, encapsulation of multiple fields into an immutable value object. That is, to replace multiple `volatile` member fields with a single `volatile` reference to the value object (or perhaps better yet an `AtomicReference` to allow for `compareAndSet` - if compare-and-set logic is appropriate). +- If it is `Serializable`, it should have a `serialVersionUID` field. Unless code has shipped to users, the initial value of the `serialVersionUID` field should be `1L`. ## Indentation @@ -32,12 +32,12 @@ Checkstyle rules are more important than this document. ## Field Naming Conventions 1. "hungarian"-style notation is banned (e.g. instance variable names preceded by an 'm', etc.). -2. If the field is `static final` then it shall be named in `ALL_CAPS_WITH_UNDERSCORES`. +2. If the field is `static final`, then it should be named as `ALL_CAPS_WITH_UNDERSCORES`. 3. Start variable names with a lowercase letter and use camelCase rather than under_scores. 4. Spelling and abbreviations: If the word is widely used in the JVM runtime, stick with the spelling/abbreviation in the JVM runtime, e.g. `color` over `colour`, `sync` over `synch`, `async` over `asynch`, etc. 5. It is acceptable to use `i`, `j`, `k` for loop indices and iterators. If you need more than three, you are likely doing something wrong and as such you shall either use full descriptive names or refactor. 6. It is acceptable to use `e` for the exception in a `try...catch` block. -7. You shall never use `l` (i.e. lower case `L`) as a variable name. +7. Never use `l` (i.e. lower case `L`) as a variable name. ## Line Length @@ -45,32 +45,33 @@ To the greatest extent possible, please wrap lines to ensure that they do not ex ## Maven POM file layout -* The `pom.xml` file shall use the sequencing of elements as defined by the `mvn tidy:pom` command (after any indenting fix-up). -* If you are introducing a property to the `pom.xml` the property must be used in at least two distinct places in the model or a comment justifying the use of a property shall be provided. -* If the `` is in the groupId `org.apache.maven.plugins` you shall omit the ``. -* All `` entries shall have an explicit version defined unless inherited from the parent. +- The `pom.xml` file should use the sequencing of elements as defined by the `mvn tidy:pom` command (after any indenting fix-up). +- If you are introducing a property to the `pom.xml`, the property must be used in at least two distinct places in the model, or a comment justifying the use of a property should be provided. +- If the `` is in the groupId `org.apache.maven.plugins`, you should omit the ``. +- All `` entries should have an explicit version defined unless inherited from the parent. ## Java code style ### Imports -* For code in `src/main`: - - `*` imports are banned. - - `static` imports are preferred until not mislead. -* For code in `src/test`: - - `*` imports of anything other than JUnit classes and Hamcrest matchers are banned. +- For code in `src/main`: + - `*` imports are banned. + - `static` imports are preferred until not mislead. +- For code in `src/test`: + - `*` imports of anything other than JUnit classes and Hamcrest matchers are banned. ### Annotation placement -* Annotations on classes, interfaces, annotations, enums, methods, fields and local variables shall be on the lines immediately preceding the line where modifier(s) (e.g. `public` / `protected` / `private` / `final`, etc) would be appropriate. -* Annotations on method arguments shall, to the largest extent possible, be on the same line as the method argument (and, if present, before the `final` modifier). +- Annotations on classes, interfaces, annotations, enums, methods, fields and local variables should be on the lines immediately preceding the line where modifier(s) (e.g. `public` / `protected` / `private` / `final`, etc) would be appropriate. +- Annotations on method arguments should, to the largest extent possible, be on the same line as the method argument (and, if present, before the `final` modifier). ### Javadoc -* Each class shall have a Javadoc comment. -* Unless the method is `private`, it shall have a Javadoc comment. -* Getters and Setters shall have a Javadoc comment. The following is prefered: - ``` +- Each class should have a Javadoc comment. +- Unless the method is `private`, it should have a Javadoc comment. +- Getters and Setters should have a Javadoc comment. The following is prefered: + + ```java /** * The count of widgets */ @@ -94,43 +95,44 @@ To the greatest extent possible, please wrap lines to ensure that they do not ex this.widgetCount = widgetCount; } ``` -* When adding a new class / interface / etc, it shall have a `@since` doc comment. The version shall be `FIXME` (or `TODO`) to indicate that the person merging the change should replace the `FIXME` with the next release version number. The fields and methods within a class/interface (but not nested classes) will be assumed to have the `@since` annotation of their class/interface unless a different `@since` annotation is present. + +- When adding a new class / interface / etc, it should have a `@since` doc comment. The version should be `FIXME` (or `TODO`) to indicate that the person merging the change should replace the `FIXME` with the next release version number. The fields and methods within a class/interface (but not nested classes) will be assumed to have the `@since` annotation of their class/interface unless a different `@since` annotation is present. ### IDE Configuration -* Eclipse, by and large the IDE defaults are acceptable with the following changes: - - Tab policy to `Spaces only`. - - Indent statements within `switch` body. - - Maximum line width `120`. - - Line wrapping, ensure all to `wrap where necessary`. - - Organize imports alphabetically, no grouping. -* NetBeans, by and large the IDE defaults are acceptable with the following changes: - - Tabs and Indents: - + Change Right Margin to `120`. - + Indent case statements in switch. - - Wrapping: - + Change all the `Never` values to `If Long`. - + Select the checkbox for Wrap After Assignment Operators. -* IntelliJ, by and large the IDE defaults are acceptable with the following changes: - - Wrapping and Braces: - + Change `Do not wrap` to `Wrap if long`. - + Change `Do not force` to `Always`. - - Javadoc: - + Disable generating `

` on empty lines. - - Imports: - + Class count to use import with '*': `9999`. - + Names count to use static import with '*': `99999`. - + Import Layout: - * import all other imports. - * blank line. - * import static all other imports. +- Eclipse: by and large the IDE defaults are acceptable with the following changes: + - Tab policy to `Spaces only`. + - Indent statements within `switch` body. + - Maximum line width `120`. + - Line wrapping, ensure all to `wrap where necessary`. + - Organize imports alphabetically, no grouping. +- NetBeans: by and large the IDE defaults are acceptable with the following changes: + - Tabs and Indents: + - Change Right Margin to `120`. + - Indent case statements in switch. + - Wrapping: + - Change all the `Never` values to `If Long`. + - Select the checkbox for Wrap After Assignment Operators. +- IntelliJ: by and large the IDE defaults are acceptable with the following changes: + - Wrapping and Braces: + - Change `Do not wrap` to `Wrap if long`. + - Change `Do not force` to `Always`. + - Javadoc: + - Disable generating `

` on empty lines. + - Imports: + - Class count to use import with '*': `9999`. + - Names count to use static import with '*': `99999`. + - Import Layout: + - import all other imports. + - blank line. + - import static all other imports. ## Issues -This project uses [Jenkins Jira issue tracker](https://issues.jenkins-ci.org) -with [github-plugin](https://issues.jenkins-ci.org/browse/JENKINS/component/15896) component. +This project uses the [Jenkins Jira issue tracker](https://issues.jenkins.io/) +with the [github-plugin](https://issues.jenkins.io/browse/JENKINS/component/15896) component. ## Links -- https://wiki.jenkins-ci.org/display/JENKINS/contributing -- https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins +- https://www.jenkins.io/participate/ +- https://www.jenkins.io/doc/developer/ diff --git a/Jenkinsfile b/Jenkinsfile index a229fa517..739042f72 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1 +1,4 @@ -buildPlugin() +buildPlugin(useContainerAgent: true, configurations: [ + [platform: 'linux', jdk: 21], + [platform: 'windows', jdk: 17], +]) diff --git a/README.md b/README.md index caeb3f3bc..2bdb9ff06 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Github Plugin +# GitHub Plugin [![codecov](https://codecov.io/gh/jenkinsci/github-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/jenkinsci/github-plugin) [![License](https://img.shields.io/github/license/jenkinsci/github-plugin.svg)](LICENSE) -This plugin integrates Jenkins with [Github](http://github.com/) +This plugin integrates Jenkins with [GitHub](http://github.com/) projects.The plugin currently has three major functionalities: - Create hyperlinks between your Jenkins projects and GitHub @@ -17,14 +17,14 @@ projects.The plugin currently has three major functionalities: ## Hyperlinks between changes -The Github plugin decorates Jenkins "Changes" pages to create links to -your Github commit and issue pages. It adds a sidebar link that links -back to the Github project page. +The GitHub plugin decorates Jenkins "Changes" pages to create links to +your GitHub commit and issue pages. It adds a sidebar link that links +back to the GitHub project page. ![](/docs/images/changes.png) ![](/docs/images/changes-2.png) -When creating a job, specify that is connects to git. Under "Github +When creating a job, specify that is connects to git. Under "GitHub project", put in: git@github.com:*Person*/*Project*.git Under "Source Code Management" select Git, and put in git@github.com:*Person*/*Project*.git @@ -36,7 +36,7 @@ repositories](https://help.github.com/post-receive-hooks/). This trigger only kicks git-plugin internal polling algo for every incoming event against matched repo. -> This plugin was previously named as "Build when a change is pushed to GitHub" +> This trigger was previously named as "Build when a change is pushed to GitHub" ## Usage @@ -103,8 +103,8 @@ only credentials that matched by predefined domains. ![](/docs/images/secret-text.png) **Step 3.** Once that configuration is done, go to the project config of -each job you want triggered automatically and simply check "Build when a -change is pushed to GitHub" under "Build Triggers". With this, every new +each job you want triggered automatically and simply check "GitHub hook trigger for GITScm polling" +under "Build Triggers". With this, every new push to the repository automatically triggers a new build. Note that there's only one URL and it receives all post-receive POSTs @@ -180,7 +180,7 @@ Additional info: Jenkins service by right clicking on Jenkins (in the services window), and hit "Restart". - Jenkins does not support passphrases for SSH keys. Therefore, if you - set one while running the initial Github configuration, rerun it and + set one while running the initial GitHub configuration, rerun it and don't set one. ## Pipeline examples @@ -204,7 +204,7 @@ void setBuildStatus(String message, String state) { setBuildStatus("Build complete", "SUCCESS"); ``` -More complex example (can be used with multiply scm sources in pipeline) +More complex example (can be used with multiple scm sources in pipeline) ```groovy def getRepoURL() { diff --git a/docs/images/changes-2.png b/docs/images/changes-2.png index de0c2ca73..e55e4e9b2 100644 Binary files a/docs/images/changes-2.png and b/docs/images/changes-2.png differ diff --git a/docs/images/changes.png b/docs/images/changes.png index abef8afca..bc8e951cd 100644 Binary files a/docs/images/changes.png and b/docs/images/changes.png differ diff --git a/docs/images/ghserver-config.png b/docs/images/ghserver-config.png index 3cb96fe75..471151457 100644 Binary files a/docs/images/ghserver-config.png and b/docs/images/ghserver-config.png differ diff --git a/docs/images/manage-token.png b/docs/images/manage-token.png index 81264e6cd..6e506bec3 100644 Binary files a/docs/images/manage-token.png and b/docs/images/manage-token.png differ diff --git a/docs/images/secret-text.png b/docs/images/secret-text.png index a30a64761..5109c4f70 100644 Binary files a/docs/images/secret-text.png and b/docs/images/secret-text.png differ diff --git a/mvnw b/mvnw index 5551fde8e..19529ddf8 100755 --- a/mvnw +++ b/mvnw @@ -19,268 +19,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - 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 +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} - saveddir=`pwd` +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - M2_HOME=`dirname "$PRG"`/.. +die() { + printf %s\\n "$1" >&2 + exit 1 +} - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} -if [ -z "$JAVACMD" ] ; then - 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" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher +mkdir -p -- "${MAVEN_HOME%/*}" -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd old mode 100755 new mode 100644 index e5cfb0ae9..249bdf382 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -18,144 +19,131 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 998b99053..9f40380f3 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.jenkins-ci.plugins plugin - 4.15 + 5.28 @@ -36,7 +36,7 @@ - scm:git:git://github.com/${gitHubRepo}.git + scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git https://github.com/${gitHubRepo} ${scmTag} @@ -47,21 +47,15 @@ - 1.33.2 + 1.45.1 -SNAPSHOT - jenkinsci/github-plugin - 2.222.4 + jenkinsci/${project.artifactId}-plugin + + 2.504 + ${jenkins.baseline}.1 false - true - 3.0.4 - 2.2 - 1 - 8 - 1.14.2 + false v@{project.version} - Low - Max - false @@ -70,7 +64,7 @@ https://repo.jenkins-ci.org/public/ - + repo.jenkins-ci.org @@ -80,14 +74,16 @@ - org.apache.commons - commons-lang3 - 3.11 + io.jenkins.plugins + commons-lang3-api + + + io.jenkins.plugins + okhttp-api org.jenkins-ci.plugins github-api - 1.114.2 @@ -123,14 +119,11 @@ org.jenkins-ci.modules instance-identity - 2.2 - provided - javax.servlet - javax.servlet-api - provided + io.jenkins.plugins + caffeine-api @@ -143,36 +136,13 @@ - org.hamcrest - hamcrest - ${hamcrest.version} - test - - - - org.hamcrest - hamcrest-core - ${hamcrest.version} - test - - - - org.hamcrest - hamcrest-library - ${hamcrest.version} - test - - - - junit - junit + org.mockito + mockito-core test - org.mockito - mockito-core - 1.10.19 + mockito-junit-jupiter test @@ -199,13 +169,6 @@ org.jenkins-ci.plugins.workflow workflow-cps test - - - - org.jenkins-ci.ui - jquery-detached - - org.jenkins-ci.plugins.workflow @@ -219,64 +182,18 @@ test - - com.tngtech.java - junit-dataprovider - 1.10.0 - test - - - com.github.tomakehurst - wiremock - 1.57 + org.wiremock + wiremock-standalone + 3.12.1 test - standalone - - - org.eclipse.jetty - jetty - - - com.google.guava - guava - - - org.apache.httpcomponents - httpclient - - - xmlunit - xmlunit - - - com.jayway.jsonpath - json-path - - - net.sf.jopt-simple - jopt-simple - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - io.rest-assured rest-assured - 4.3.3 + 5.3.2 test @@ -286,16 +203,11 @@ io.jenkins.tools.bom - bom-2.222.x - 20 + bom-${jenkins.baseline}.x + 4710.v016f0a_07e34d import pom - - org.jenkins-ci - annotation-indexer - 1.12 - @@ -319,7 +231,7 @@ maven-checkstyle-plugin - 3.1.1 + 3.6.0 checkstyle @@ -330,7 +242,6 @@ - UTF-8 true true false diff --git a/src/main/java/com/cloudbees/jenkins/Cleaner.java b/src/main/java/com/cloudbees/jenkins/Cleaner.java index 182ece08e..027083192 100644 --- a/src/main/java/com/cloudbees/jenkins/Cleaner.java +++ b/src/main/java/com/cloudbees/jenkins/Cleaner.java @@ -34,7 +34,7 @@ public class Cleaner extends PeriodicWork { * This queue is thread-safe, so any thread can write or * fetch names to this queue without additional sync */ - private final Queue cleanQueue = new ConcurrentLinkedQueue(); + private final Queue cleanQueue = new ConcurrentLinkedQueue<>(); /** * Called when a {@link GitHubPushTrigger} is about to be removed. @@ -61,8 +61,7 @@ protected void doRun() throws Exception { URL url = GitHubPlugin.configuration().getHookUrl(); - List items = Jenkins.getInstance().getAllItems(Item.class); - List aliveRepos = from(items) + List aliveRepos = from(Jenkins.get().allItems(Item.class)) .filter(isAlive()) // live repos .transformAndConcat(associatedNames()).toList(); diff --git a/src/main/java/com/cloudbees/jenkins/Credential.java b/src/main/java/com/cloudbees/jenkins/Credential.java index d5b801a7b..99e766119 100644 --- a/src/main/java/com/cloudbees/jenkins/Credential.java +++ b/src/main/java/com/cloudbees/jenkins/Credential.java @@ -7,7 +7,7 @@ import org.kohsuke.github.GitHub; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.CheckForNull; +import edu.umd.cs.findbugs.annotations.CheckForNull; import java.io.IOException; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; diff --git a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java index a0e662024..9d7663e51 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubCommitNotifier.java @@ -32,12 +32,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; import java.io.IOException; import java.util.Collections; import static com.cloudbees.jenkins.Messages.GitHubCommitNotifier_DisplayName; -import static com.google.common.base.Objects.firstNonNull; import static hudson.model.Result.FAILURE; import static hudson.model.Result.SUCCESS; import static hudson.model.Result.UNSTABLE; @@ -93,17 +91,17 @@ public void setStatusMessage(ExpandableMessage statusMessage) { /** * @since 1.10 */ - @Nonnull + @NonNull public String getResultOnFailure() { return resultOnFailure != null ? resultOnFailure : getDefaultResultOnFailure().toString(); } - @Nonnull + @NonNull public static Result getDefaultResultOnFailure() { return FAILURE; } - @Nonnull + @NonNull /*package*/ Result getEffectiveResultOnFailure() { return Result.fromString(trimToEmpty(resultOnFailure)); } @@ -125,7 +123,7 @@ public void perform(@NonNull Run build, setter.setContextSource(new DefaultCommitContextSource()); - String content = firstNonNull(statusMessage, DEFAULT_MESSAGE).getContent(); + String content = (statusMessage != null ? statusMessage : DEFAULT_MESSAGE).getContent(); if (isNotBlank(content)) { setter.setStatusResultSource(new ConditionalStatusResultSource( diff --git a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java index 62259c733..4cae5f049 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubPushTrigger.java @@ -2,6 +2,7 @@ import com.google.common.base.Charsets; import com.google.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Util; import hudson.XmlFile; @@ -38,8 +39,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.inject.Inject; +import edu.umd.cs.findbugs.annotations.NonNull; +import jakarta.inject.Inject; import java.io.File; import java.io.IOException; import java.io.PrintStream; @@ -85,7 +86,7 @@ public void onPost() { */ public void onPost(String triggeredByUser) { onPost(GitHubTriggerEvent.create() - .withOrigin(SCMEvent.originOf(Stapler.getCurrentRequest())) + .withOrigin(SCMEvent.originOf(Stapler.getCurrentRequest2())) .withTriggeredByUser(triggeredByUser) .build() ); @@ -182,7 +183,7 @@ public File getLogFile() { /** * Returns the file that records the last/current polling activity. */ - private File getLogFileForJob(@Nonnull Job job) throws IOException { + private File getLogFileForJob(@NonNull Job job) throws IOException { return new File(job.getRootDir(), "github-polling.log"); } @@ -262,7 +263,7 @@ public String getUrlName() { } public String getLog() throws IOException { - return Util.loadFile(getLogFileForJob(job)); + return Util.loadFile(getLogFileForJob(Objects.requireNonNull(job))); } /** @@ -270,8 +271,16 @@ public String getLog() throws IOException { * * @since 1.350 */ + @SuppressFBWarnings( + value = "RV_RETURN_VALUE_IGNORED", + justification = + "method signature does not permit plumbing through the return value") public void writeLogTo(XMLOutput out) throws IOException { - new AnnotatedLargeText(getLogFileForJob(job), Charsets.UTF_8, true, this) + new AnnotatedLargeText( + getLogFileForJob(Objects.requireNonNull(job)), + Charsets.UTF_8, + true, + this) .writeHtmlTo(0, out.asWriter()); } } diff --git a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java index 242fc8851..5cdb857b3 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubRepositoryName.java @@ -17,8 +17,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -46,22 +46,22 @@ public class GitHubRepositoryName { * from URLs that include a '.git' suffix, removing the suffix from the * repository name. */ - Pattern.compile("git@(.+):([^/]+)/([^/]+)\\.git(?:/)?"), + Pattern.compile(".+@(.+):([^/]+)/([^/]+)\\.git(?:/)?"), Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"), Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"), Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"), - Pattern.compile("(?:git\\+)?ssh://(?:git@)?([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"), + Pattern.compile("(?:git\\+)?ssh://(?:.+@)?([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"), /** * The second set of patterns extract the host, owner and repository names * from all other URLs. Note that these patterns must be processed *after* * the first set, to avoid any '.git' suffix that may be present being included * in the repository name. */ - Pattern.compile("git@(.+):([^/]+)/([^/]+)/?"), + Pattern.compile(".+@(.+):([^/]+)/([^/]+)/?"), Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"), Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"), Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"), - Pattern.compile("(?:git\\+)?ssh://(?:git@)?([^/]+)/([^/]+)/([^/]+)/?"), + Pattern.compile("(?:git\\+)?ssh://(?:.+@)?([^/]+)/([^/]+)/([^/]+)/?"), }; /** @@ -83,7 +83,7 @@ public static GitHubRepositoryName create(String url) { return ret; } } - LOGGER.warn("Could not match URL {}", url); + LOGGER.debug("Could not match URL {}", url); return null; } @@ -222,7 +222,7 @@ public String toString() { private static Function toGHRepository(final GitHubRepositoryName repoName) { return new NullSafeFunction() { @Override - protected GHRepository applyNullSafe(@Nonnull GitHub gitHub) { + protected GHRepository applyNullSafe(@NonNull GitHub gitHub) { try { return gitHub.getRepository(format("%s/%s", repoName.getUserName(), repoName.getRepositoryName())); } catch (IOException e) { diff --git a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java index 862d41955..f30ff9136 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilder.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.util.Collections; -import static com.google.common.base.Objects.firstNonNull; import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; import static org.jenkinsci.plugins.github.status.sources.misc.AnyBuildResult.onAnyResult; @@ -89,7 +88,7 @@ public void perform(@NonNull Run build, Collections.singletonList( onAnyResult( GHCommitState.PENDING, - defaultIfEmpty(firstNonNull(statusMessage, DEFAULT_MESSAGE).getContent(), + defaultIfEmpty((statusMessage != null ? statusMessage : DEFAULT_MESSAGE).getContent(), Messages.CommitNotifier_Pending(build.getDisplayName())) ) ))); diff --git a/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java b/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java index 364631c9e..fdae66124 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubTriggerEvent.java @@ -1,5 +1,6 @@ package com.cloudbees.jenkins; +import jakarta.servlet.http.HttpServletRequest; import jenkins.scm.api.SCMEvent; /** @@ -14,7 +15,7 @@ public class GitHubTriggerEvent { */ private final long timestamp; /** - * The origin of the event (see {@link SCMEvent#originOf(javax.servlet.http.HttpServletRequest)}) + * The origin of the event (see {@link SCMEvent#originOf(HttpServletRequest)}) */ private final String origin; /** diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java index 3033771a2..887a1a366 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHook.java @@ -25,7 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.net.URL; import java.util.List; @@ -52,6 +52,12 @@ public class GitHubWebHook implements UnprotectedRootAction { // headers used for testing the endpoint configuration public static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation"; public static final String X_INSTANCE_IDENTITY = "X-Instance-Identity"; + /** + * X-GitHub-Delivery: A globally unique identifier (GUID) to identify the event. + * @see Delivery + * headers + */ + public static final String X_GITHUB_DELIVERY = "X-GitHub-Delivery"; private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting); @@ -116,9 +122,11 @@ public List reRegisterAllHooks() { */ @SuppressWarnings("unused") @RequirePostWithGHHookPayload - public void doIndex(@Nonnull @GHEventHeader GHEvent event, @Nonnull @GHEventPayload String payload) { + public void doIndex(@NonNull @GHEventHeader GHEvent event, @NonNull @GHEventPayload String payload) { + var currentRequest = Stapler.getCurrentRequest2(); + String eventGuid = currentRequest.getHeader(X_GITHUB_DELIVERY); GHSubscriberEvent subscriberEvent = - new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest()), event, payload); + new GHSubscriberEvent(eventGuid, SCMEvent.originOf(currentRequest), event, payload); from(GHEventsSubscriber.all()) .filter(isInterestedIn(event)) .transform(processEvent(subscriberEvent)).toList(); @@ -149,7 +157,7 @@ public static GitHubWebHook get() { return Jenkins.getInstance().getExtensionList(RootAction.class).get(GitHubWebHook.class); } - @Nonnull + @NonNull public static Jenkins getJenkinsInstance() throws IllegalStateException { Jenkins instance = Jenkins.getInstance(); Validate.validState(instance != null, "Jenkins has not been started, or was already shut down"); diff --git a/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java b/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java index e342e1261..39191f388 100644 --- a/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java +++ b/src/main/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusion.java @@ -3,10 +3,10 @@ import hudson.Extension; import hudson.security.csrf.CrumbExclusion; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import static org.apache.commons.lang3.StringUtils.isEmpty; @@ -21,7 +21,7 @@ public boolean process(HttpServletRequest req, HttpServletResponse resp, FilterC if (isEmpty(pathInfo)) { return false; } - // Github will not follow redirects https://github.com/isaacs/github/issues/574 + // GitHub will not follow redirects https://github.com/isaacs/github/issues/574 pathInfo = pathInfo.endsWith("/") ? pathInfo : pathInfo + '/'; if (!pathInfo.equals(getExclusionPath())) { return false; diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java index b92c5f4b4..662b714cb 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAction.java @@ -10,9 +10,9 @@ import java.util.Collections; /** - * Add the Github Logo/Icon to the sidebar. + * Add the GitHub Logo/Icon to the sidebar. * - * @author Stefan Saasen + * @author Stefan Saasen */ public final class GithubLinkAction implements Action { @@ -29,7 +29,7 @@ public String getDisplayName() { @Override public String getIconFileName() { - return "/plugin/github/logov3.png"; + return "symbol-logo-github plugin-github"; } @Override diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java index 388901f02..d96acee40 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubLinkAnnotator.java @@ -7,9 +7,20 @@ import hudson.plugins.git.GitChangeSet; import hudson.scm.ChangeLogAnnotator; import hudson.scm.ChangeLogSet.Entry; +import org.apache.commons.lang3.StringUtils; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; + +import static hudson.Functions.htmlAttributeEscape; import static java.lang.String.format; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.regex.Pattern; /** @@ -17,15 +28,22 @@ *

* It's based on the TracLinkAnnotator. *

- * - * @author Stefan Saasen - * @todo Change the annotator to use GithubUrl instead of the String url. + * TODO Change the annotator to use GithubUrl instead of the String url. * Knowledge about the github url structure should be encapsulated in * GithubUrl. + * + * @author Stefan Saasen */ @Extension public class GithubLinkAnnotator extends ChangeLogAnnotator { + private static final Set ALLOWED_URI_SCHEMES = new HashSet(); + + static { + ALLOWED_URI_SCHEMES.addAll( + Arrays.asList("http", "https")); + } + @Override public void annotate(Run build, Entry change, MarkupText text) { final GithubProjectProperty p = build.getParent().getProperty( @@ -38,15 +56,18 @@ public void annotate(Run build, Entry change, MarkupText text) { void annotate(final GithubUrl url, final MarkupText text, final Entry change) { final String base = url.baseUrl(); + boolean isValid = verifyUrl(base); + if (!isValid) { + throw new IllegalArgumentException("The provided GitHub URL is not valid"); + } for (LinkMarkup markup : MARKUPS) { markup.process(text, base); } - if (change instanceof GitChangeSet) { GitChangeSet cs = (GitChangeSet) change; final String id = cs.getId(); text.wrapBy("", format(" (commit: %s)", - url.commitId(id), + htmlAttributeEscape(url.commitId(id)), id.substring(0, Math.min(id.length(), 7)))); } } @@ -66,7 +87,7 @@ private static final class LinkMarkup { void process(MarkupText text, String url) { for (SubText st : text.findTokens(pattern)) { - st.surroundWith("", ""); + st.surroundWith("", ""); } } @@ -77,5 +98,35 @@ void process(MarkupText text, String url) { private static final LinkMarkup[] MARKUPS = new LinkMarkup[]{new LinkMarkup( "(?:C|c)lose(?:s?)\\s(? + * @author Stefan Saasen */ public final class GithubProjectProperty extends JobProperty> { @@ -88,7 +89,7 @@ public void setDisplayName(String displayName) { * @return display name or full job name if field is not defined * @since 1.14.1 */ - public static String displayNameFor(@Nonnull Job job) { + public static String displayNameFor(@NonNull Job job) { GithubProjectProperty ghProp = job.getProperty(GithubProjectProperty.class); if (ghProp != null && isNotBlank(ghProp.getDisplayName())) { return ghProp.getDisplayName(); @@ -98,6 +99,7 @@ public static String displayNameFor(@Nonnull Job job) { } @Extension + @Symbol("githubProjectProperty") public static final class DescriptorImpl extends JobPropertyDescriptor { /** * Used to hide property configuration under checkbox, @@ -114,7 +116,9 @@ public String getDisplayName() { } @Override - public JobProperty newInstance(@Nonnull StaplerRequest req, JSONObject formData) throws FormException { + public JobProperty newInstance(@NonNull StaplerRequest2 req, + JSONObject formData) throws Descriptor.FormException { + GithubProjectProperty tpp = req.bindJSON( GithubProjectProperty.class, formData.getJSONObject(GITHUB_PROJECT_BLOCK_NAME) @@ -135,5 +139,5 @@ public JobProperty newInstance(@Nonnull StaplerRequest req, JSONObject formDa } - private static final Logger LOGGER = Logger.getLogger(GitHubPushTrigger.class.getName()); + private static final Logger LOGGER = Logger.getLogger(GithubProjectProperty.class.getName()); } diff --git a/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java b/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java index b331adcb3..50e9ad9ed 100644 --- a/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java +++ b/src/main/java/com/coravy/hudson/plugins/github/GithubUrl.java @@ -1,9 +1,9 @@ package com.coravy.hudson.plugins.github; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** - * @author Stefan Saasen + * @author Stefan Saasen */ public final class GithubUrl { diff --git a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java index 383f82203..4a45fbd2a 100644 --- a/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java +++ b/src/main/java/org/jenkinsci/plugins/github/GitHubPlugin.java @@ -6,7 +6,9 @@ import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.migration.Migrator; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; @@ -23,6 +25,8 @@ public class GitHubPlugin extends Plugin { * Launched before plugin starts * Adds alias for {@link GitHubPlugin} to simplify resulting xml. */ + @Initializer(before = InitMilestone.SYSTEM_CONFIG_LOADED) + @Restricted(DoNotUse.class) public static void addXStreamAliases() { Migrator.enableCompatibilityAliases(); Migrator.enableAliases(); @@ -39,17 +43,12 @@ public static void runMigrator() throws Exception { new Migrator().migrate(); } - @Override - public void start() throws Exception { - addXStreamAliases(); - } - /** * Shortcut method for getting instance of {@link GitHubPluginConfig}. * * @return configuration of plugin */ - @Nonnull + @NonNull public static GitHubPluginConfig configuration() { return defaultIfNull( GitHubPluginConfig.all().get(GitHubPluginConfig.class), diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java b/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java index a96f2d189..52eeb6fef 100644 --- a/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java +++ b/src/main/java/org/jenkinsci/plugins/github/admin/GHRepoName.java @@ -3,7 +3,7 @@ import com.cloudbees.jenkins.GitHubRepositoryName; import org.kohsuke.stapler.AnnotationHandler; import org.kohsuke.stapler.InjectedParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.slf4j.Logger; import java.lang.annotation.Documented; @@ -37,8 +37,8 @@ class PayloadHandler extends AnnotationHandler { * @return {@link GitHubRepositoryName} extracted from request or null on any problem */ @Override - public GitHubRepositoryName parse(StaplerRequest req, GHRepoName a, Class type, String param) { - String repo = notNull(req, "Why StaplerRequest is null?").getParameter(param); + public GitHubRepositoryName parse(StaplerRequest2 req, GHRepoName a, Class type, String param) { + String repo = notNull(req, "Why StaplerRequest2 is null?").getParameter(param); LOGGER.trace("Repo url in method {}", repo); return GitHubRepositoryName.create(repo); } diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java new file mode 100644 index 000000000..794f3db04 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java @@ -0,0 +1,216 @@ +package org.jenkinsci.plugins.github.admin; + +import java.time.Duration; +import java.time.Instant; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Ticker; +import com.google.common.annotations.VisibleForTesting; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.AdministrativeMonitor; +import hudson.model.Item; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.Messages; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.github.GHEvent; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.WebMethod; +import org.kohsuke.stapler.json.JsonHttpResponse; +import org.kohsuke.stapler.verb.GET; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.sf.json.JSONObject; + +@SuppressWarnings("unused") +@Extension +public class GitHubDuplicateEventsMonitor extends AdministrativeMonitor { + + @VisibleForTesting + static final String LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID = GitHubDuplicateEventsMonitor.class.getName() + + ".last-duplicate"; + + @Override + public String getDisplayName() { + return Messages.duplicate_events_administrative_monitor_displayname(); + } + + public String getDescription() { + return Messages.duplicate_events_administrative_monitor_description(); + } + + public String getBlurb() { + return Messages.duplicate_events_administrative_monitor_blurb( + LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID, this.getLastDuplicateUrl()); + } + + @VisibleForTesting + String getLastDuplicateUrl() { + return this.getUrl() + "/" + "last-duplicate.json"; + } + + @Override + public boolean isActivated() { + return ExtensionList.lookupSingleton(DuplicateEventsSubscriber.class).isDuplicateEventSeen(); + } + + @Override + public boolean hasRequiredPermission() { + return Jenkins.get().hasPermission(Jenkins.SYSTEM_READ); + } + + @Override + public void checkRequiredPermission() { + Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); + } + + @GET + @WebMethod(name = "last-duplicate.json") + public HttpResponse doGetLastDuplicatePayload() { + Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); + JSONObject data; + var lastDuplicate = ExtensionList.lookupSingleton(DuplicateEventsSubscriber.class).getLastDuplicate(); + if (lastDuplicate != null) { + data = JSONObject.fromObject(lastDuplicate.ghSubscriberEvent().getPayload()); + } else { + data = getLastDuplicateNoEventPayload(); + } + return new JsonHttpResponse(data, 200); + } + + @VisibleForTesting + static JSONObject getLastDuplicateNoEventPayload() { + return new JSONObject().accumulate("payload", "No duplicate events seen yet"); + } + + /** + * Tracks duplicate {@link GHEvent} triggering actions in Jenkins. + * Events are tracked for 10 minutes, with the last detected duplicate reference retained for up to 24 hours + * (see {@link #isDuplicateEventSeen}). + *

+ * Duplicates are stored in-memory only, so a controller restart clears all entries as if none existed. + * Persistent storage is omitted for simplicity, since webhook misconfigurations would likely cause new duplicates. + */ + @Extension + public static final class DuplicateEventsSubscriber extends GHEventsSubscriber { + + private static final Logger LOGGER = Logger.getLogger(DuplicateEventsSubscriber.class.getName()); + + private Ticker ticker = Ticker.systemTicker(); + /** + * Caches GitHub event GUIDs for 10 minutes to track recent events to detect duplicates. + *

+ * Only the keys (event GUIDs) are relevant, as Caffeine automatically handles expiration based + * on insertion time; the value is irrelevant, we put {@link #DUMMY}, as Caffeine doesn't provide any + * Set structures. + *

+ * Maximum cache size is set to 24k so it doesn't grow unbound (approx. 1MB). Each key takes 36 bytes, and + * timestamp (assuming caffeine internally keeps long) takes 8 bytes; total of 44 bytes + * per entry. So the maximum memory consumed by this cache is 24k * 44 = 1056k = 1.056 MB. + */ + private final Cache eventTracker = Caffeine.newBuilder() + .maximumSize(24_000L) + .expireAfterWrite(Duration.ofMinutes(10)) + .ticker(() -> ticker.read()) + .build(); + private static final Object DUMMY = new Object(); + + private volatile TrackedDuplicateEvent lastDuplicate; + public record TrackedDuplicateEvent( + String eventGuid, Instant lastUpdated, GHSubscriberEvent ghSubscriberEvent) { } + private static final Duration TWENTY_FOUR_HOURS = Duration.ofHours(24); + + @VisibleForTesting + @Restricted(NoExternalUse.class) + void setTicker(Ticker testTicker) { + ticker = testTicker; + } + + /** + * This subscriber is not applicable to any item + * + * @param item ignored + * @return always false + */ + @Override + protected boolean isApplicable(@Nullable Item item) { + return false; + } + + /** + * {@inheritDoc} + *

+ * Subscribes to events that trigger actions in Jenkins, such as repository scans or builds. + *

+ * The {@link GHEvent} enum defines about 63 events, but not all are relevant to Jenkins. + * Tracking unnecessary events increases memory usage, and they occur more frequently than those triggering any + * work. + *

+ * + * Documentation reference (also referenced in {@link GHEvent}) + */ + @Override + protected Set events() { + return Set.of( + GHEvent.CHECK_RUN, // associated with GitHub action Re-run button to trigger build + GHEvent.CHECK_SUITE, // associated with GitHub action Re-run button to trigger build + GHEvent.CREATE, // branch or tag creation + GHEvent.DELETE, // branch or tag deletion + GHEvent.PULL_REQUEST, // PR creation (also PR close or merge) + GHEvent.PUSH // commit push + ); + } + + @Override + protected void onEvent(final GHSubscriberEvent event) { + String eventGuid = event.getEventGuid(); + LOGGER.fine(() -> "Received event with GUID: " + eventGuid); + if (eventGuid == null) { + return; + } + if (eventTracker.getIfPresent(eventGuid) != null) { + lastDuplicate = new TrackedDuplicateEvent(eventGuid, getNow(), event); + } + eventTracker.put(eventGuid, DUMMY); + } + + /** + * Checks if a duplicate event was recorded in the past 24 hours. + *

+ * Events are not stored for 24 hours—only the most recent duplicate is checked within this timeframe. + * + * @return {@code true} if a duplicate was seen in the last 24 hours, {@code false} otherwise. + */ + public boolean isDuplicateEventSeen() { + return lastDuplicate != null + && Duration.between(lastDuplicate.lastUpdated(), getNow()).compareTo(TWENTY_FOUR_HOURS) < 0; + } + + private Instant getNow() { + return Instant.ofEpochSecond(0L, ticker.read()); + } + + public TrackedDuplicateEvent getLastDuplicate() { + return lastDuplicate; + } + + /** + * Caffeine expired keys are not removed immediately. Method returns the non-expired keys; + * required for the tests. + */ + @VisibleForTesting + @Restricted(NoExternalUse.class) + Set getPresentEventKeys() { + return eventTracker.asMap().keySet().stream() + .filter(key -> eventTracker.getIfPresent(key) != null) + .collect(Collectors.toSet()); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java index d502eff59..33dad11a9 100644 --- a/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java +++ b/src/main/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitor.java @@ -15,13 +15,13 @@ import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.interceptor.RequirePOST; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.inject.Inject; +import edu.umd.cs.findbugs.annotations.NonNull; +import jakarta.inject.Inject; import java.io.File; import java.io.IOException; import java.util.List; @@ -32,7 +32,7 @@ /** * Administrative monitor to track problems of registering/removing hooks for GH. - * Holds non-savable map of repo->message and persisted list of ignored projects. + * Holds non-savable map of repo->message and persisted list of ignored projects. * Anyone can register new problem with {@link #registerProblem(GitHubRepositoryName, Throwable)} and check * repo for problems with {@link #isProblemWith(GitHubRepositoryName)} * @@ -64,7 +64,7 @@ public GitHubHookRegisterProblemMonitor() { } /** - * @return Immutable copy of map with repo->problem message content + * @return Immutable copy of map with repo->problem message content */ public Map getProblems() { return ImmutableMap.copyOf(problems); @@ -147,7 +147,7 @@ public boolean isActivated() { */ @RequirePOST @RequireAdminRights - public HttpResponse doAct(StaplerRequest req) throws IOException { + public HttpResponse doAct(StaplerRequest2 req) throws IOException { if (req.hasParameter("no")) { disable(true); return HttpResponses.redirectViaContextPath("/manage"); @@ -166,7 +166,7 @@ public HttpResponse doAct(StaplerRequest req) throws IOException { @ValidateRepoName @RequireAdminRights @RespondWithRedirect - public void doIgnore(@Nonnull @GHRepoName GitHubRepositoryName repo) { + public void doIgnore(@NonNull @GHRepoName GitHubRepositoryName repo) { if (!ignored.contains(repo)) { ignored.add(repo); } @@ -183,7 +183,7 @@ public void doIgnore(@Nonnull @GHRepoName GitHubRepositoryName repo) { @ValidateRepoName @RequireAdminRights @RespondWithRedirect - public void doDisignore(@Nonnull @GHRepoName GitHubRepositoryName repo) { + public void doDisignore(@NonNull @GHRepoName GitHubRepositoryName repo) { ignored.remove(repo); } @@ -236,7 +236,7 @@ public static class GitHubHookRegisterProblemManagementLink extends ManagementLi public String getIconFileName() { return monitor.getProblems().isEmpty() && monitor.ignored.isEmpty() ? null - : "/plugin/github/img/logo.svg"; + : "symbol-logo-github plugin-github"; } @Override diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java b/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java index 00a9617cc..953a2fae0 100644 --- a/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java +++ b/src/main/java/org/jenkinsci/plugins/github/admin/RequireAdminRights.java @@ -1,12 +1,12 @@ package org.jenkinsci.plugins.github.admin; import jenkins.model.Jenkins; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.Interceptor; import org.kohsuke.stapler.interceptor.InterceptorAnnotation; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; @@ -29,7 +29,7 @@ class Processor extends Interceptor { @Override - public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments) + public Object invoke(StaplerRequest2 request, StaplerResponse2 response, Object instance, Object[] arguments) throws IllegalAccessException, InvocationTargetException, ServletException { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java b/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java index bfc4a196d..f0be54946 100644 --- a/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java +++ b/src/main/java/org/jenkinsci/plugins/github/admin/RespondWithRedirect.java @@ -1,12 +1,12 @@ package org.jenkinsci.plugins.github.admin; import org.kohsuke.stapler.HttpRedirect; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.Interceptor; import org.kohsuke.stapler.interceptor.InterceptorAnnotation; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; @@ -30,7 +30,7 @@ class Processor extends Interceptor { @Override - public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments) + public Object invoke(StaplerRequest2 request, StaplerResponse2 response, Object instance, Object[] arguments) throws IllegalAccessException, InvocationTargetException, ServletException { target.invoke(request, response, instance, arguments); throw new InvocationTargetException(new HttpRedirect(".")); diff --git a/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java b/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java index 6a7d6a3ba..b4977e418 100644 --- a/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java +++ b/src/main/java/org/jenkinsci/plugins/github/admin/ValidateRepoName.java @@ -1,12 +1,12 @@ package org.jenkinsci.plugins.github.admin; import com.cloudbees.jenkins.GitHubRepositoryName; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.Interceptor; import org.kohsuke.stapler.interceptor.InterceptorAnnotation; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; @@ -16,7 +16,7 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; import static org.kohsuke.stapler.HttpResponses.errorWithoutStack; @@ -34,7 +34,7 @@ class Processor extends Interceptor { @Override - public Object invoke(StaplerRequest request, StaplerResponse response, Object instance, Object[] arguments) + public Object invoke(StaplerRequest2 request, StaplerResponse2 response, Object instance, Object[] arguments) throws IllegalAccessException, InvocationTargetException, ServletException { if (!from(newArrayList(arguments)).firstMatch(instanceOf(GitHubRepositoryName.class)).isPresent()) { diff --git a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java index 71fec736e..b155a57c3 100644 --- a/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/common/CombineErrorHandler.java @@ -5,7 +5,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.List; @@ -53,7 +53,7 @@ public CombineErrorHandler withHandlers(List handlers) { * @return true if exception handled or rethrows it */ @Override - public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + public boolean handle(Exception e, @NonNull Run run, @NonNull TaskListener listener) { LOG.debug("Exception in {} will be processed with {} handlers", run.getParent().getName(), handlers.size(), e); try { diff --git a/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java index 65c4104f1..235caa1db 100644 --- a/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/common/ErrorHandler.java @@ -3,7 +3,7 @@ import hudson.model.Run; import hudson.model.TaskListener; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * So you can implement bunch of {@link ErrorHandler}s and log, rethrow, ignore exception. @@ -26,5 +26,5 @@ public interface ErrorHandler { * @return true if exception handled successfully * @throws Exception you can rethrow exception of any type */ - boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) throws Exception; + boolean handle(Exception e, @NonNull Run run, @NonNull TaskListener listener) throws Exception; } diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java index f3bb9304f..44ee71060 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubPluginConfig.java @@ -4,11 +4,14 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.BulkChange; import hudson.Extension; import hudson.Util; import hudson.XmlFile; import hudson.model.Descriptor; import hudson.model.Item; +import hudson.security.Permission; import hudson.util.FormValidation; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; @@ -24,12 +27,12 @@ import org.kohsuke.github.GitHub; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.interceptor.RequirePOST; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -189,18 +192,36 @@ protected XmlFile getConfigFile() { } @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { - hookSecretConfigs = null; // form binding might omit empty lists + public boolean configure(StaplerRequest2 req, JSONObject json) throws FormException { try { - req.bindJSON(this, json); + BulkChange bc = new BulkChange(this); + try { + if (json.has("configs")) { + setConfigs(req.bindJSONToList(GitHubServerConfig.class, json.get("configs"))); + } else { + setConfigs(Collections.emptyList()); + } + if (json.has("hookSecretConfigs")) { + setHookSecretConfigs(req.bindJSONToList(HookSecretConfig.class, json.get("hookSecretConfigs"))); + } else { + setHookSecretConfigs(Collections.emptyList()); + } + if (json.optBoolean("isOverrideHookUrl", false) && (json.has("hookUrl"))) { + setHookUrl(json.optString("hookUrl")); + } else { + setHookUrl(null); + } + req.bindJSON(this, json); + clearRedundantCaches(configs); + } finally { + bc.commit(); + } } catch (Exception e) { LOGGER.debug("Problem while submitting form for GitHub Plugin ({})", e.getMessage(), e); LOGGER.trace("GH form data: {}", json.toString()); throw new FormException( format("Malformed GitHub Plugin configuration (%s)", e.getMessage()), e, "github-configuration"); } - save(); - clearRedundantCaches(configs); return true; } @@ -212,7 +233,7 @@ public String getDisplayName() { @SuppressWarnings("unused") @RequirePOST public FormValidation doReRegister() { - Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE); if (!GitHubPlugin.configuration().isManageHooks()) { return FormValidation.warning("Works only when Jenkins manages hooks (one or more creds specified)"); } @@ -227,7 +248,7 @@ public FormValidation doReRegister() { @Restricted(DoNotUse.class) // WebOnly @SuppressWarnings("unused") public FormValidation doCheckHookUrl(@QueryParameter String value) { - Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE); try { HttpURLConnection con = (HttpURLConnection) new URL(value).openConnection(); con.setRequestMethod("POST"); @@ -317,4 +338,10 @@ private URL parseHookUrl(String hookUrl) { return null; } } + + @NonNull + @Override + public Permission getRequiredGlobalConfigPagePermission() { + return Jenkins.MANAGE; + } } diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java index 88bb78ce5..9fed6de8d 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubServerConfig.java @@ -7,12 +7,14 @@ import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Supplier; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.security.ACL; +import hudson.security.Permission; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.Secret; @@ -21,8 +23,6 @@ import java.net.URL; import java.util.Collections; import java.util.List; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; import jenkins.model.Jenkins; import jenkins.scm.api.SCMName; import org.apache.commons.lang3.StringUtils; @@ -215,7 +215,7 @@ public int getClientCacheSize() { } /** - * @param clientCacheSize capacity of cache for GitHub client in MB, set to <= 0 to turn off this feature + * @param clientCacheSize capacity of cache for GitHub client in MB, set to <= 0 to turn off this feature */ @DataBoundSetter public void setClientCacheSize(int clientCacheSize) { @@ -268,7 +268,7 @@ public static Function loginToGithub() { * * @return token from creds or default non empty string */ - @Nonnull + @NonNull public static String tokenFor(String credentialsId) { return secretFor(credentialsId).or(new Supplier() { @Override @@ -285,7 +285,7 @@ public Secret get() { * * @return secret from creds or empty optional */ - @Nonnull + @NonNull public static Optional secretFor(String credentialsId) { List creds = filter( lookupCredentials(StringCredentials.class, @@ -297,7 +297,7 @@ public static Optional secretFor(String credentialsId) { return FluentIterableWrapper.from(creds) .transform(new NullSafeFunction() { @Override - protected Secret applyNullSafe(@Nonnull StringCredentials input) { + protected Secret applyNullSafe(@NonNull StringCredentials input) { return input.getSecret(); } }).first(); @@ -318,7 +318,7 @@ protected Secret applyNullSafe(@Nonnull StringCredentials input) { public static Predicate withHost(final String host) { return new NullSafePredicate() { @Override - protected boolean applyNullSafe(@Nonnull GitHubServerConfig github) { + protected boolean applyNullSafe(@NonNull GitHubServerConfig github) { return defaultIfEmpty(github.getApiUrl(), GITHUB_URL).contains(host); } }; @@ -346,10 +346,16 @@ public String getDisplayName() { return "GitHub Server"; } + @NonNull + @Override + public Permission getRequiredGlobalConfigPagePermission() { + return Jenkins.MANAGE; + } + @SuppressWarnings("unused") public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl, @QueryParameter String credentialsId) { - if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + if (!Jenkins.getInstance().hasPermission(Jenkins.MANAGE)) { return new StandardListBoxModel().includeCurrentValue(credentialsId); } return new StandardListBoxModel() @@ -368,7 +374,7 @@ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl, public FormValidation doVerifyCredentials( @QueryParameter String apiUrl, @QueryParameter String credentialsId) throws IOException { - Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE); GitHubServerConfig config = new GitHubServerConfig(credentialsId); config.setApiUrl(apiUrl); @@ -413,11 +419,13 @@ public FormValidation doCheckApiUrl(@QueryParameter String value) { */ private static class ClientCacheFunction extends NullSafeFunction { @Override - protected GitHub applyNullSafe(@Nonnull GitHubServerConfig github) { + protected GitHub applyNullSafe(@NonNull GitHubServerConfig github) { if (github.getCachedClient() == null) { github.setCachedClient(new GitHubLoginFunction().apply(github)); } return github.getCachedClient(); } } + + } diff --git a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java index 46947b4f2..38cbb73ed 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/GitHubTokenCredentialsCreator.java @@ -28,8 +28,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; import java.net.URI; import java.util.List; @@ -92,7 +92,7 @@ public String getDisplayName() { @SuppressWarnings("unused") public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl, @QueryParameter String credentialsId) { - if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + if (!Jenkins.getInstance().hasPermission(Jenkins.MANAGE)) { return new StandardUsernameListBoxModel().includeCurrentValue(credentialsId); } return new StandardUsernameListBoxModel() @@ -118,7 +118,7 @@ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl, @Que public FormValidation doCreateTokenByCredentials( @QueryParameter String apiUrl, @QueryParameter String credentialsId) { - Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE); if (isEmpty(credentialsId)) { return FormValidation.error("Please specify credentials to create token"); } @@ -167,7 +167,7 @@ public FormValidation doCreateTokenByPassword( @QueryParameter String apiUrl, @QueryParameter String login, @QueryParameter String password) { - Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER); + Jenkins.getActiveInstance().checkPermission(Jenkins.MANAGE); try { GHAuthorization token = createToken(login, password, defaultIfBlank(apiUrl, GITHUB_URL)); StandardCredentials credentials = createCredentials(apiUrl, token.getToken(), login); @@ -190,8 +190,8 @@ public FormValidation doCreateTokenByPassword( * @return personal token with requested scope * @throws IOException when can't create token with given creds */ - public GHAuthorization createToken(@Nonnull String username, - @Nonnull String password, + public GHAuthorization createToken(@NonNull String username, + @NonNull String password, @Nullable String apiUrl) throws IOException { GitHub gitHub = new GitHubBuilder() .withEndpoint(defaultIfBlank(apiUrl, GITHUB_URL)) @@ -236,7 +236,7 @@ public StandardCredentials createCredentials(@Nullable String serverAPIUrl, Stri * * @return saved creds */ - private StandardCredentials createCredentials(@Nonnull String serverAPIUrl, + private StandardCredentials createCredentials(@NonNull String serverAPIUrl, final StandardCredentials credentials) { URI serverUri = URI.create(defaultIfBlank(serverAPIUrl, GITHUB_URL)); diff --git a/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java index f50815ad1..9db733af7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java +++ b/src/main/java/org/jenkinsci/plugins/github/config/HookSecretConfig.java @@ -3,17 +3,20 @@ import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.security.ACL; +import hudson.security.Permission; import hudson.util.ListBoxModel; import hudson.util.Secret; import jenkins.model.Jenkins; +import org.jenkinsci.plugins.github.webhook.SignatureAlgorithm; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nullable; +import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Collections; import org.kohsuke.stapler.QueryParameter; @@ -23,10 +26,19 @@ public class HookSecretConfig extends AbstractDescribableImpl { private String credentialsId; + private SignatureAlgorithm signatureAlgorithm; @DataBoundConstructor - public HookSecretConfig(String credentialsId) { + public HookSecretConfig(String credentialsId, String signatureAlgorithm) { this.credentialsId = credentialsId; + this.signatureAlgorithm = parseSignatureAlgorithm(signatureAlgorithm); + } + + /** + * Legacy constructor for backwards compatibility. + */ + public HookSecretConfig(String credentialsId) { + this(credentialsId, null); } /** @@ -43,6 +55,26 @@ public String getCredentialsId() { return credentialsId; } + /** + * Gets the signature algorithm to use for webhook validation. + * + * @return the configured signature algorithm, defaults to SHA-256 + * @since 1.45.0 + */ + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm != null ? signatureAlgorithm : SignatureAlgorithm.getDefault(); + } + + /** + * Gets the signature algorithm name for UI binding. + * + * @return the algorithm name as string (e.g., "SHA256", "SHA1") + * @since 1.45.0 + */ + public String getSignatureAlgorithmName() { + return getSignatureAlgorithm().name(); + } + /** * @param credentialsId a new ID * @deprecated rather treat this field as final and use {@link GitHubPluginConfig#setHookSecretConfigs} @@ -52,6 +84,33 @@ public void setCredentialsId(String credentialsId) { this.credentialsId = credentialsId; } + /** + * Ensures backwards compatibility during deserialization. + * Sets default algorithm to SHA-256 for existing configurations. + */ + private Object readResolve() { + if (signatureAlgorithm == null) { + signatureAlgorithm = SignatureAlgorithm.getDefault(); + } + return this; + } + + /** + * Parses signature algorithm from UI string input. + */ + private SignatureAlgorithm parseSignatureAlgorithm(String algorithmName) { + if (algorithmName == null || algorithmName.trim().isEmpty()) { + return SignatureAlgorithm.getDefault(); + } + + try { + return SignatureAlgorithm.valueOf(algorithmName.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + // Default to SHA-256 for invalid input + return SignatureAlgorithm.getDefault(); + } + } + @Extension public static class DescriptorImpl extends Descriptor { @@ -60,9 +119,19 @@ public String getDisplayName() { return "Hook secret configuration"; } + /** + * Provides dropdown items for signature algorithm selection. + */ + public ListBoxModel doFillSignatureAlgorithmItems() { + ListBoxModel items = new ListBoxModel(); + items.add("SHA-256 (Recommended)", "SHA256"); + items.add("SHA-1 (Legacy)", "SHA1"); + return items; + } + @SuppressWarnings("unused") public ListBoxModel doFillCredentialsIdItems(@QueryParameter String credentialsId) { - if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + if (!Jenkins.getInstance().hasPermission(Jenkins.MANAGE)) { return new StandardListBoxModel().includeCurrentValue(credentialsId); } @@ -76,5 +145,11 @@ public ListBoxModel doFillCredentialsIdItems(@QueryParameter String credentialsI CredentialsMatchers.always() ); } + + @NonNull + @Override + public Permission getRequiredGlobalConfigPagePermission() { + return Jenkins.MANAGE; + } } } diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java index eb458a186..155d8c826 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java @@ -8,7 +8,7 @@ import hudson.model.Job; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import javax.annotation.CheckForNull; +import edu.umd.cs.findbugs.annotations.CheckForNull; import jenkins.model.Jenkins; import jenkins.scm.api.SCMEvent; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; @@ -18,8 +18,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + import java.util.Collections; import java.util.Set; @@ -156,7 +157,7 @@ public static ExtensionList all() { public static Function> extractEvents() { return new NullSafeFunction>() { @Override - protected Set applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { + protected Set applyNullSafe(@NonNull GHEventsSubscriber subscriber) { return defaultIfNull(subscriber.events(), Collections.emptySet()); } }; @@ -188,7 +189,7 @@ public static Predicate isApplicableFor(final Job proj public static Predicate isApplicableFor(final Item item) { return new NullSafePredicate() { @Override - protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { + protected boolean applyNullSafe(@NonNull GHEventsSubscriber subscriber) { return subscriber.safeIsApplicable(item); } }; @@ -204,7 +205,7 @@ protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { public static Predicate isInterestedIn(final GHEvent event) { return new NullSafePredicate() { @Override - protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { + protected boolean applyNullSafe(@NonNull GHEventsSubscriber subscriber) { return defaultIfNull(subscriber.events(), emptySet()).contains(event); } }; @@ -221,7 +222,7 @@ protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { */ @Deprecated public static Function processEvent(final GHEvent event, final String payload) { - return processEvent(new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest()), event, payload)); + return processEvent(new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest2()), event, payload)); } /** @@ -235,7 +236,7 @@ public static Function processEvent(final GHEvent even public static Function processEvent(final GHSubscriberEvent event) { return new NullSafeFunction() { @Override - protected Void applyNullSafe(@Nonnull GHEventsSubscriber subscriber) { + protected Void applyNullSafe(@NonNull GHEventsSubscriber subscriber) { try { subscriber.onEvent(event); } catch (Throwable t) { diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java b/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java index f5fc752cc..bde28d6f1 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/GHSubscriberEvent.java @@ -1,10 +1,11 @@ package org.jenkinsci.plugins.github.extension; +import jakarta.servlet.http.HttpServletRequest; import jenkins.scm.api.SCMEvent; import org.kohsuke.github.GHEvent; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * An event for a {@link GHEventsSubscriber}. @@ -17,16 +18,32 @@ public class GHSubscriberEvent extends SCMEvent { */ private final GHEvent ghEvent; + private final String eventGuid; + + /** + * @deprecated use {@link #GHSubscriberEvent(String, String, GHEvent, String)} instead. + */ + @Deprecated + public GHSubscriberEvent(@CheckForNull String origin, @NonNull GHEvent ghEvent, @NonNull String payload) { + this(null, origin, ghEvent, payload); + } + /** * Constructs a new {@link GHSubscriberEvent}. - * - * @param origin the origin (see {@link SCMEvent#originOf(javax.servlet.http.HttpServletRequest)}) or {@code null}. + * @param eventGuid the globally unique identifier (GUID) to identify the event; value of + * request header {@link com.cloudbees.jenkins.GitHubWebHook#X_GITHUB_DELIVERY}. + * @param origin the origin (see {@link SCMEvent#originOf(HttpServletRequest)}) or {@code null}. * @param ghEvent the type of event received from GitHub. * @param payload the event payload. */ - public GHSubscriberEvent(@CheckForNull String origin, @Nonnull GHEvent ghEvent, @Nonnull String payload) { + public GHSubscriberEvent( + @CheckForNull String eventGuid, + @CheckForNull String origin, + @NonNull GHEvent ghEvent, + @NonNull String payload) { super(Type.UPDATED, payload, origin); this.ghEvent = ghEvent; + this.eventGuid = eventGuid; } /** @@ -38,4 +55,8 @@ public GHEvent getGHEvent() { return ghEvent; } + @CheckForNull + public String getEventGuid() { + return eventGuid; + } } diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java index 325261387..5b118fa1c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubCommitShaSource.java @@ -5,7 +5,7 @@ import hudson.model.Run; import hudson.model.TaskListener; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; /** @@ -23,6 +23,6 @@ public abstract class GitHubCommitShaSource extends AbstractDescribableImpl run, @Nonnull TaskListener listener) + public abstract String get(@NonNull Run run, @NonNull TaskListener listener) throws IOException, InterruptedException; } diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java index fa21c9bd9..c231297f7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubReposSource.java @@ -6,7 +6,7 @@ import hudson.model.TaskListener; import org.kohsuke.github.GHRepository; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; /** @@ -23,5 +23,5 @@ public abstract class GitHubReposSource extends AbstractDescribableImpl repos(@Nonnull Run run, @Nonnull TaskListener listener); + public abstract List repos(@NonNull Run run, @NonNull TaskListener listener); } diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java index f359f1810..bc307d6c7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusContextSource.java @@ -5,7 +5,7 @@ import hudson.model.Run; import hudson.model.TaskListener; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * Extension point to provide context of the state. For example `integration-tests` or `build` @@ -22,5 +22,5 @@ public abstract class GitHubStatusContextSource extends AbstractDescribableImpl< * * @return simple short string to represent context of this state */ - public abstract String context(@Nonnull Run run, @Nonnull TaskListener listener); + public abstract String context(@NonNull Run run, @NonNull TaskListener listener); } diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java index 81a14b811..620864120 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/GitHubStatusResultSource.java @@ -6,7 +6,7 @@ import hudson.model.TaskListener; import org.kohsuke.github.GHCommitState; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; /** @@ -24,7 +24,7 @@ public abstract class GitHubStatusResultSource extends AbstractDescribableImpl run, @Nonnull TaskListener listener) + public abstract StatusResult get(@NonNull Run run, @NonNull TaskListener listener) throws IOException, InterruptedException; /** diff --git a/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java b/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java index c1486b331..cfc9dc624 100644 --- a/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java +++ b/src/main/java/org/jenkinsci/plugins/github/extension/status/misc/ConditionalResult.java @@ -10,7 +10,7 @@ import org.kohsuke.github.GHCommitState; import org.kohsuke.stapler.DataBoundSetter; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * This extension point allows to define when and what to send as state and message. @@ -56,7 +56,7 @@ public String getMessage() { * * @return true if matches */ - public abstract boolean matches(@Nonnull Run run); + public abstract boolean matches(@NonNull Run run); /** * Should be extended to and marked as {@link hudson.Extension} to be in list diff --git a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java index 60a7e5aa9..7ea4b69a3 100644 --- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOps.java @@ -4,7 +4,7 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.hash.Hashing; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import edu.umd.cs.findbugs.annotations.NonNull; import okhttp3.Cache; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.github.GitHubPlugin; @@ -14,9 +14,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Path; import java.util.List; @@ -95,7 +95,6 @@ public static Path getBaseCacheDir() { * * @param configs active server configs to exclude caches from cleanup */ - @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE") public static void clearRedundantCaches(List configs) { Path baseCacheDir = getBaseCacheDir(); @@ -134,7 +133,7 @@ private static void deleteEveryIn(DirectoryStream caches) { */ private static class WithEnabledCache extends NullSafePredicate { @Override - protected boolean applyNullSafe(@Nonnull GitHubServerConfig config) { + protected boolean applyNullSafe(@NonNull GitHubServerConfig config) { return config.getClientCacheSize() > 0; } } @@ -147,7 +146,7 @@ private static class ToCacheDir extends NullSafeFunction 0, "Cache can't be with size <= 0"); Path cacheDir = getBaseCacheDir().resolve(hashed(config)); @@ -161,8 +160,8 @@ protected Cache applyNullSafe(@Nonnull GitHubServerConfig config) { */ private static String hashed(GitHubServerConfig config) { return Hashing.murmur3_32().newHasher() - .putString(trimToEmpty(config.getApiUrl())) - .putString(trimToEmpty(config.getCredentialsId())).hash().toString(); + .putString(trimToEmpty(config.getApiUrl()), StandardCharsets.UTF_8) + .putString(trimToEmpty(config.getCredentialsId()), StandardCharsets.UTF_8).hash().toString(); } } @@ -171,7 +170,7 @@ private static String hashed(GitHubServerConfig config) { */ private static class CacheToName extends NullSafeFunction { @Override - protected String applyNullSafe(@Nonnull Cache cache) { + protected String applyNullSafe(@NonNull Cache cache) { return cache.directory().getName(); } } diff --git a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java index dd5cb728b..ecee2d33b 100644 --- a/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java +++ b/src/main/java/org/jenkinsci/plugins/github/internal/GitHubLoginFunction.java @@ -1,6 +1,7 @@ package org.jenkinsci.plugins.github.internal; import com.cloudbees.jenkins.GitHubWebHook; +import io.jenkins.plugins.okhttp.api.JenkinsOkHttpClient; import okhttp3.Cache; import okhttp3.OkHttpClient; import jenkins.model.Jenkins; @@ -15,8 +16,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.net.MalformedURLException; import java.net.Proxy; @@ -44,7 +45,7 @@ @Restricted(NoExternalUse.class) public class GitHubLoginFunction extends NullSafeFunction { - private static final OkHttpClient BASECLIENT = new OkHttpClient(); + private static final OkHttpClient BASECLIENT = JenkinsOkHttpClient.newClientBuilder(new OkHttpClient()).build(); private static final Logger LOGGER = LoggerFactory.getLogger(GitHubLoginFunction.class); /** @@ -57,7 +58,7 @@ public class GitHubLoginFunction extends NullSafeFunction getErrorHandlers() { * Gets info from the providers and updates commit status */ @Override - public void perform(@Nonnull Run run, @Nonnull FilePath workspace, @Nonnull Launcher launcher, - @Nonnull TaskListener listener) { + public void perform(@NonNull Run run, @NonNull FilePath workspace, @NonNull Launcher launcher, + @NonNull TaskListener listener) { try { String sha = getCommitShaSource().get(run, listener); List repos = getReposSource().repos(run, listener); diff --git a/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java index 1400f9822..348f4084c 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/err/ChangingBuildStatusErrorHandler.java @@ -9,7 +9,7 @@ import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import static hudson.model.Result.FAILURE; import static hudson.model.Result.UNSTABLE; @@ -40,7 +40,7 @@ public String getResult() { * @return true as of it terminating handler */ @Override - public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + public boolean handle(Exception e, @NonNull Run run, @NonNull TaskListener listener) { Result toSet = Result.fromString(trimToEmpty(result)); listener.error("[GitHub Commit Status Setter] - %s, setting build result to %s", e.getMessage(), toSet); diff --git a/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java b/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java index ed389b7dc..4fb544526 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/err/ShallowAnyErrorHandler.java @@ -7,7 +7,7 @@ import org.jenkinsci.plugins.github.extension.status.StatusErrorHandler; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * Just logs message to the build console and do nothing after it @@ -25,7 +25,7 @@ public ShallowAnyErrorHandler() { * @return true as of its terminating handler */ @Override - public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + public boolean handle(Exception e, @NonNull Run run, @NonNull TaskListener listener) { listener.error("[GitHub Commit Status Setter] Failed to update commit state on GitHub. " + "Ignoring exception [%s]", e.getMessage()); return true; diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java index 5183de388..b0333d88b 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/AnyDefinedRepositorySource.java @@ -13,7 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Collection; import java.util.List; @@ -37,7 +37,7 @@ public AnyDefinedRepositorySource() { * @return all repositories which can be found by repo-contributors */ @Override - public List repos(@Nonnull Run run, @Nonnull TaskListener listener) { + public List repos(@NonNull Run run, @NonNull TaskListener listener) { final Collection names = GitHubRepositoryNameContributor .parseAssociatedNames(run.getParent()); @@ -45,7 +45,7 @@ public List repos(@Nonnull Run run, @Nonnull TaskListener li return from(names).transformAndConcat(new NullSafeFunction>() { @Override - protected Iterable applyNullSafe(@Nonnull GitHubRepositoryName name) { + protected Iterable applyNullSafe(@NonNull GitHubRepositoryName name) { return name.resolve(); } }).toList(); diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java index 126122b67..bdec8c467 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/BuildDataRevisionShaSource.java @@ -9,7 +9,7 @@ import org.jenkinsci.plugins.github.util.BuildDataHelper; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; /** @@ -28,7 +28,7 @@ public BuildDataRevisionShaSource() { * @return sha from git's scm build data action */ @Override - public String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException { + public String get(@NonNull Run run, @NonNull TaskListener listener) throws IOException { return ObjectId.toString(BuildDataHelper.getCommitSHA1(run)); } diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java index 268ee604b..2c7cd6cb5 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSource.java @@ -11,7 +11,7 @@ import org.kohsuke.github.GHCommitState; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.util.Collections; import java.util.List; @@ -34,7 +34,7 @@ public ConditionalStatusResultSource(List results) { this.results = results; } - @Nonnull + @NonNull public List getResults() { return defaultIfNull(results, Collections.emptyList()); } @@ -46,7 +46,7 @@ public List getResults() { * @return first matched result or pending state with warn msg */ @Override - public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) + public StatusResult get(@NonNull Run run, @NonNull TaskListener listener) throws IOException, InterruptedException { for (ConditionalResult conditionalResult : getResults()) { diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java index fbd1d3ccb..ee4a38694 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultCommitContextSource.java @@ -7,7 +7,7 @@ import org.jenkinsci.plugins.github.extension.status.GitHubStatusContextSource; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import static com.coravy.hudson.plugins.github.GithubProjectProperty.displayNameFor; @@ -28,7 +28,7 @@ public DefaultCommitContextSource() { * @see com.coravy.hudson.plugins.github.GithubProjectProperty#displayNameFor(hudson.model.Job) */ @Override - public String context(@Nonnull Run run, @Nonnull TaskListener listener) { + public String context(@NonNull Run run, @NonNull TaskListener listener) { return displayNameFor(run.getParent()); } diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java index c33971aff..e1a1176f7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSource.java @@ -10,7 +10,7 @@ import org.kohsuke.github.GHCommitState; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import static hudson.model.Result.FAILURE; @@ -34,7 +34,7 @@ public DefaultStatusResultSource() { } @Override - public StatusResult get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException, + public StatusResult get(@NonNull Run run, @NonNull TaskListener listener) throws IOException, InterruptedException { // We do not use `build.getDurationString()` because it appends 'and counting' (build is still running) diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java index ee28e2dd7..ae7768918 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredCommitContextSource.java @@ -10,7 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * Allows to manually enter context @@ -36,7 +36,7 @@ public String getContext() { * Just returns what user entered. Expands env vars and token macro */ @Override - public String context(@Nonnull Run run, @Nonnull TaskListener listener) { + public String context(@NonNull Run run, @NonNull TaskListener listener) { try { return new ExpandableMessage(context).expandAll(run, listener); } catch (Exception e) { diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java index 0a73f04f3..3493321b2 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySource.java @@ -11,7 +11,7 @@ import org.kohsuke.github.GHRepository; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Collections; import java.util.List; @@ -35,11 +35,11 @@ GitHubRepositoryName createName(String url) { } @Override - public List repos(@Nonnull Run run, @Nonnull final TaskListener listener) { + public List repos(@NonNull Run run, @NonNull final TaskListener listener) { List urls = Collections.singletonList(url); return from(urls).transformAndConcat(new NullSafeFunction>() { @Override - protected Iterable applyNullSafe(@Nonnull String url) { + protected Iterable applyNullSafe(@NonNull String url) { GitHubRepositoryName name = createName(url); if (name != null) { return name.resolve(); diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java index 74b353f45..a6055a863 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredShaSource.java @@ -8,7 +8,7 @@ import org.jenkinsci.plugins.github.extension.status.GitHubCommitShaSource; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; /** @@ -34,7 +34,7 @@ public String getSha() { * Expands env vars and token macro in entered sha */ @Override - public String get(@Nonnull Run run, @Nonnull TaskListener listener) throws IOException, InterruptedException { + public String get(@NonNull Run run, @NonNull TaskListener listener) throws IOException, InterruptedException { return new ExpandableMessage(sha).expandAll(run, listener); } diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java index 947db9075..1f1dcb7fc 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResult.java @@ -6,7 +6,7 @@ import org.kohsuke.github.GHCommitState; import org.kohsuke.stapler.DataBoundConstructor; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; /** * Allows to set state in any case @@ -24,7 +24,7 @@ public AnyBuildResult() { * @return true in any case */ @Override - public boolean matches(@Nonnull Run run) { + public boolean matches(@NonNull Run run) { return true; } diff --git a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java index 9600e4b22..8fcd53185 100644 --- a/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java +++ b/src/main/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResult.java @@ -9,7 +9,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import static hudson.model.Result.FAILURE; import static hudson.model.Result.SUCCESS; @@ -45,7 +45,7 @@ public String getResult() { * @return matches if run result better than or equal to selected */ @Override - public boolean matches(@Nonnull Run run) { + public boolean matches(@NonNull Run run) { return defaultIfNull(run.getResult(), Result.NOT_BUILT).isBetterOrEqualTo(fromString(trimToEmpty(result))); } diff --git a/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java b/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java index 118437ec8..b4a8e72bd 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/BuildDataHelper.java @@ -7,7 +7,7 @@ import hudson.plugins.git.util.BuildData; import org.eclipse.jgit.lib.ObjectId; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.util.List; import java.util.Set; @@ -15,7 +15,7 @@ /** * Stores common methods for {@link BuildData} handling. * - * @author Oleg Nenashev + * @author Oleg Nenashev * @since 1.10 */ public final class BuildDataHelper { @@ -73,8 +73,8 @@ public static BuildData calculateBuildData( * @return SHA1 of the las * @throws IOException Cannot get the info about commit ID */ - @Nonnull - public static ObjectId getCommitSHA1(@Nonnull Run build) throws IOException { + @NonNull + public static ObjectId getCommitSHA1(@NonNull Run build) throws IOException { List buildDataList = build.getActions(BuildData.class); Job parent = build.getParent(); diff --git a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java index 8babf4b23..4ccfcde28 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/FluentIterableWrapper.java @@ -26,10 +26,11 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; -import javax.annotation.CheckReturnValue; import java.util.Iterator; import java.util.List; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; + import static com.google.common.base.Preconditions.checkNotNull; /** diff --git a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java index c935f2f43..eafbc2c39 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/JobInfoHelpers.java @@ -13,12 +13,11 @@ import jenkins.model.ParameterizedJobMixIn; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; -import javax.annotation.CheckForNull; +import edu.umd.cs.findbugs.annotations.CheckForNull; import java.util.Collection; import java.util.Map; import static org.jenkinsci.plugins.github.extension.GHEventsSubscriber.isApplicableFor; -import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** * Utility class which holds converters or predicates (matchers) to filter or convert job lists @@ -38,11 +37,7 @@ private JobInfoHelpers() { * @return predicate with true on apply if job contains trigger of given class */ public static Predicate withTrigger(final Class clazz) { - return new Predicate() { - public boolean apply(Item item) { - return triggerFrom(item, clazz) != null; - } - }; + return item -> triggerFrom(item, clazz) != null; } /** @@ -51,22 +46,14 @@ public boolean apply(Item item) { * @return predicate with true on apply if item is buildable */ public static Predicate isBuildable() { - return new Predicate() { - public boolean apply(ITEM item) { - return item instanceof Job ? ((Job) item).isBuildable() : item instanceof BuildableItem; - } - }; + return item -> item instanceof Job ? ((Job) item).isBuildable() : item instanceof BuildableItem; } /** * @return function which helps to convert job to repo names associated with this job */ public static Function> associatedNames() { - return new Function>() { - public Collection apply(ITEM item) { - return GitHubRepositoryNameContributor.parseAssociatedNames(item); - } - }; + return GitHubRepositoryNameContributor::parseAssociatedNames; } /** @@ -76,12 +63,7 @@ public Collection apply(ITEM item) { * @return predicate with true if item alive and should have hook */ public static Predicate isAlive() { - return new Predicate() { - @Override - public boolean apply(ITEM item) { - return !from(GHEventsSubscriber.all()).filter(isApplicableFor(item)).toList().isEmpty(); - } - }; + return item -> GHEventsSubscriber.all().stream().anyMatch(isApplicableFor(item)); } /** diff --git a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java index 4ba1df548..3a0918247 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafeFunction.java @@ -2,7 +2,7 @@ import com.google.common.base.Function; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import static com.google.common.base.Preconditions.checkNotNull; @@ -21,5 +21,5 @@ public T apply(F input) { /** * This method will be called inside of {@link #apply(Object)} */ - protected abstract T applyNullSafe(@Nonnull F input); + protected abstract T applyNullSafe(@NonNull F input); } diff --git a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java index 5e9987d7c..847753d59 100644 --- a/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java +++ b/src/main/java/org/jenkinsci/plugins/github/util/misc/NullSafePredicate.java @@ -2,7 +2,7 @@ import com.google.common.base.Predicate; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import static com.google.common.base.Preconditions.checkNotNull; @@ -22,5 +22,5 @@ public boolean apply(T input) { /** * This method will be called inside of {@link #apply(Object)} */ - protected abstract boolean applyNullSafe(@Nonnull T input); + protected abstract boolean applyNullSafe(@NonNull T input); } diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java index b17f82116..71d19fed6 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventHeader.java @@ -3,10 +3,10 @@ import org.kohsuke.github.GHEvent; import org.kohsuke.stapler.AnnotationHandler; import org.kohsuke.stapler.InjectedParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.slf4j.Logger; -import javax.servlet.ServletException; +import jakarta.servlet.ServletException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -42,7 +42,7 @@ class PayloadHandler extends AnnotationHandler { * @return parsed {@link GHEvent} or null on empty header or unknown value */ @Override - public Object parse(StaplerRequest req, GHEventHeader a, Class type, String param) throws ServletException { + public Object parse(StaplerRequest2 req, GHEventHeader a, Class type, String param) throws ServletException { isTrue(GHEvent.class.isAssignableFrom(type), "Parameter '%s' should has type %s, not %s", param, GHEvent.class.getName(), diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java index 51e5ecb62..f7f192503 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHEventPayload.java @@ -8,11 +8,11 @@ import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.kohsuke.stapler.AnnotationHandler; import org.kohsuke.stapler.InjectedParameter; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.slf4j.Logger; -import javax.annotation.Nonnull; -import javax.servlet.ServletException; +import edu.umd.cs.findbugs.annotations.NonNull; +import jakarta.servlet.ServletException; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -46,8 +46,8 @@ class PayloadHandler extends AnnotationHandler { * * @see Developer manual */ - private static final Map> PAYLOAD_PROCESS = - ImmutableMap.>builder() + private static final Map> PAYLOAD_PROCESS = + ImmutableMap.>builder() .put(APPLICATION_JSON, fromApplicationJson()) .put(FORM_URLENCODED, fromForm()) .build(); @@ -58,8 +58,8 @@ class PayloadHandler extends AnnotationHandler { * @return String payload extracted from request or null on any problem */ @Override - public Object parse(StaplerRequest req, GHEventPayload a, Class type, String param) throws ServletException { - if (notNull(req, "Why StaplerRequest is null?").getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { + public Object parse(StaplerRequest2 req, GHEventPayload a, Class type, String param) throws ServletException { + if (notNull(req, "Why StaplerRequest2 is null?").getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { // if self test for custom hook url return null; } @@ -82,10 +82,10 @@ public Object parse(StaplerRequest req, GHEventPayload a, Class type, String par * * @return function to extract payload from form request parameters */ - protected static Function fromForm() { - return new NullSafeFunction() { + protected static Function fromForm() { + return new NullSafeFunction() { @Override - protected String applyNullSafe(@Nonnull StaplerRequest request) { + protected String applyNullSafe(@NonNull StaplerRequest2 request) { return request.getParameter("payload"); } }; @@ -96,10 +96,10 @@ protected String applyNullSafe(@Nonnull StaplerRequest request) { * * @return function to extract payload from body */ - protected static Function fromApplicationJson() { - return new NullSafeFunction() { + protected static Function fromApplicationJson() { + return new NullSafeFunction() { @Override - protected String applyNullSafe(@Nonnull StaplerRequest request) { + protected String applyNullSafe(@NonNull StaplerRequest2 request) { try { return IOUtils.toString(request.getInputStream(), Charsets.UTF_8); } catch (IOException e) { diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java b/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java index 5d434a682..491223c76 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignature.java @@ -2,13 +2,14 @@ import hudson.util.Secret; import org.apache.commons.codec.binary.Hex; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; + import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; @@ -22,6 +23,7 @@ public class GHWebhookSignature { private static final Logger LOGGER = LoggerFactory.getLogger(GHWebhookSignature.class); private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; + private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256"; public static final String INVALID_SIGNATURE = "COMPUTED_INVALID_SIGNATURE"; private final String payload; @@ -46,19 +48,42 @@ public static GHWebhookSignature webhookSignature(String payload, Secret secret) /** * Computes a RFC 2104-compliant HMAC digest using SHA1 of a payloadFrom with a given key (secret). * + * @deprecated Use {@link #sha256()} for enhanced security * @return HMAC digest of payloadFrom using secret as key. Will return COMPUTED_INVALID_SIGNATURE * on any exception during computation. */ + @Deprecated public String sha1() { + return computeSignature(HMAC_SHA1_ALGORITHM); + } + + /** + * Computes a RFC 2104-compliant HMAC digest using SHA256 of a payload with a given key (secret). + * This is the recommended method for webhook signature validation. + * + * @return HMAC digest of payload using secret as key. Will return COMPUTED_INVALID_SIGNATURE + * on any exception during computation. + * @since 1.45.0 + */ + public String sha256() { + return computeSignature(HMAC_SHA256_ALGORITHM); + } + /** + * Computes HMAC signature using the specified algorithm. + * + * @param algorithm The HMAC algorithm to use (e.g., "HmacSHA1", "HmacSHA256") + * @return HMAC digest as hex string, or INVALID_SIGNATURE on error + */ + private String computeSignature(String algorithm) { try { - final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), HMAC_SHA1_ALGORITHM); - final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); + final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), algorithm); + final Mac mac = Mac.getInstance(algorithm); mac.init(keySpec); final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(UTF_8)); return Hex.encodeHexString(rawHMACBytes); } catch (Exception e) { - LOGGER.error("", e); + LOGGER.error("Error computing {} signature", algorithm, e); return INVALID_SIGNATURE; } } @@ -67,10 +92,45 @@ public String sha1() { * @param digest computed signature from external place (GitHub) * * @return true if computed and provided signatures identical + * @deprecated Use {@link #matches(String, SignatureAlgorithm)} for explicit algorithm selection */ + @Deprecated public boolean matches(String digest) { - String computed = sha1(); - LOGGER.trace("Signature: calculated={} provided={}", computed, digest); - return StringUtils.equals(computed, digest); + return matches(digest, SignatureAlgorithm.SHA1); + } + + /** + * Validates a signature using the specified algorithm. + * Uses constant-time comparison to prevent timing attacks. + * + * @param digest the signature to validate (without algorithm prefix) + * @param algorithm the signature algorithm to use + * @return true if computed and provided signatures match + * @since 1.45.0 + */ + public boolean matches(String digest, SignatureAlgorithm algorithm) { + String computed; + switch (algorithm) { + case SHA256: + computed = sha256(); + break; + case SHA1: + computed = sha1(); + break; + default: + LOGGER.warn("Unsupported signature algorithm: {}", algorithm); + return false; + } + + LOGGER.trace("Signature validation: algorithm={} calculated={} provided={}", + algorithm, computed, digest); + if (digest == null && computed == null) { + return true; + } else if (digest == null || computed == null) { + return false; + } else { + // Use constant-time comparison to prevent timing attacks + return MessageDigest.isEqual(computed.getBytes(UTF_8), digest.getBytes(UTF_8)); + } } } diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java index 5ff8c790a..9a36c06f7 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayload.java @@ -10,14 +10,14 @@ import org.jenkinsci.plugins.github.util.FluentIterableWrapper; import org.kohsuke.github.GHEvent; import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.Interceptor; import org.kohsuke.stapler.interceptor.InterceptorAnnotation; import org.slf4j.Logger; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.annotation.Retention; @@ -27,8 +27,6 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.RSAPublicKey; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; import static com.cloudbees.jenkins.GitHubWebHook.X_INSTANCE_IDENTITY; import static com.google.common.base.Charsets.UTF_8; @@ -37,8 +35,8 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; import static org.apache.commons.codec.binary.Base64.encodeBase64; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.substringAfter; @@ -61,15 +59,26 @@ class Processor extends Interceptor { private static final Logger LOGGER = getLogger(Processor.class); /** - * Header key being used for the payload signatures. + * Header key being used for the legacy SHA-1 payload signatures. * * @see Developer manual + * @deprecated Use SHA-256 signatures with X-Hub-Signature-256 header */ + @Deprecated public static final String SIGNATURE_HEADER = "X-Hub-Signature"; - private static final String SHA1_PREFIX = "sha1="; + /** + * Header key being used for the SHA-256 payload signatures (recommended). + * + * @see + * GitHub Documentation + * @since 1.45.0 + */ + public static final String SIGNATURE_HEADER_SHA256 = "X-Hub-Signature-256"; + public static final String SHA1_PREFIX = "sha1="; + public static final String SHA256_PREFIX = "sha256="; @Override - public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, Object[] arguments) + public Object invoke(StaplerRequest2 req, StaplerResponse2 rsp, Object instance, Object[] arguments) throws IllegalAccessException, InvocationTargetException, ServletException { shouldBePostMethod(req); @@ -81,13 +90,13 @@ public Object invoke(StaplerRequest req, StaplerResponse rsp, Object instance, O } /** - * Duplicates {@link @org.kohsuke.stapler.interceptor.RequirePOST} precheck. + * Duplicates {@link org.kohsuke.stapler.interceptor.RequirePOST} precheck. * As of it can't guarantee order of multiply interceptor calls, * it should implement all features of required interceptors in one class * * @throws InvocationTargetException if method os not POST */ - protected void shouldBePostMethod(StaplerRequest request) throws InvocationTargetException { + protected void shouldBePostMethod(StaplerRequest2 request) throws InvocationTargetException { if (!request.getMethod().equals("POST")) { throw new InvocationTargetException(error(SC_METHOD_NOT_ALLOWED, "Method POST required")); } @@ -96,12 +105,12 @@ protected void shouldBePostMethod(StaplerRequest request) throws InvocationTarge /** * Used for {@link GitHubPluginConfig#doCheckHookUrl(String)}} */ - protected void returnsInstanceIdentityIfLocalUrlTest(StaplerRequest req) throws InvocationTargetException { + protected void returnsInstanceIdentityIfLocalUrlTest(StaplerRequest2 req) throws InvocationTargetException { if (req.getHeader(GitHubWebHook.URL_VALIDATION_HEADER) != null) { // when the configuration page provides the self-check button, it makes a request with this header. throw new InvocationTargetException(new HttpResponses.HttpResponseException() { @Override - public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) + public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) throws IOException, ServletException { RSAPublicKey key = new InstanceIdentity().getPublic(); rsp.setStatus(HttpServletResponse.SC_OK); @@ -139,24 +148,66 @@ protected void shouldContainParseablePayload(Object[] arguments) throws Invocati * if a hook secret is specified in the GitHub plugin config. * If no hook secret is configured, then the signature is ignored. * + * Uses the configured signature algorithm (SHA-256 by default, SHA-1 for legacy support). + * * @param req Incoming request. * @throws InvocationTargetException if any of preconditions is not satisfied */ - protected void shouldProvideValidSignature(StaplerRequest req, Object[] args) throws InvocationTargetException { - List secrets = GitHubPlugin.configuration().getHookSecretConfigs().stream(). - map(HookSecretConfig::getHookSecret).filter(Objects::nonNull).collect(Collectors.toList()); - - if (!secrets.isEmpty()) { - Optional signHeader = Optional.fromNullable(req.getHeader(SIGNATURE_HEADER)); - isTrue(signHeader.isPresent(), "Signature was expected, but not provided"); - - String digest = substringAfter(signHeader.get(), SHA1_PREFIX); - LOGGER.trace("Trying to verify sign from header {}", signHeader.get()); - isTrue( - secrets.stream().anyMatch(secret -> - GHWebhookSignature.webhookSignature(payloadFrom(req, args), secret).matches(digest)), - String.format("Provided signature [%s] did not match to calculated", digest) - ); + protected void shouldProvideValidSignature(StaplerRequest2 req, Object[] args) + throws InvocationTargetException { + List secretConfigs = GitHubPlugin.configuration().getHookSecretConfigs(); + + if (!secretConfigs.isEmpty()) { + boolean validSignatureFound = false; + + for (HookSecretConfig config : secretConfigs) { + Secret secret = config.getHookSecret(); + if (secret == null) { + continue; + } + + SignatureAlgorithm algorithm = config.getSignatureAlgorithm(); + String headerName = algorithm.getHeaderName(); + String expectedPrefix = algorithm.getSignaturePrefix(); + + Optional signHeader = Optional.fromNullable(req.getHeader(headerName)); + if (!signHeader.isPresent()) { + LOGGER.debug("No signature header {} found for algorithm {}", headerName, algorithm); + continue; + } + + String fullSignature = signHeader.get(); + if (!fullSignature.startsWith(expectedPrefix)) { + LOGGER.debug("Signature header {} does not start with expected prefix {}", + fullSignature, expectedPrefix); + continue; + } + + String digest = substringAfter(fullSignature, expectedPrefix); + LOGGER.trace("Verifying {} signature from header {}", algorithm, fullSignature); + + boolean isValid = GHWebhookSignature.webhookSignature(payloadFrom(req, args), secret) + .matches(digest, algorithm); + + if (isValid) { + validSignatureFound = true; + // Log deprecation warning for SHA-1 usage + if (algorithm == SignatureAlgorithm.SHA1) { + LOGGER.warn("Using deprecated SHA-1 signature validation. " + + "Consider upgrading webhook configuration to use SHA-256 " + + "for enhanced security."); + } else { + LOGGER.debug("Successfully validated {} signature", algorithm); + } + break; + } else { + LOGGER.debug("Signature validation failed for algorithm {}", algorithm); + } + } + + isTrue(validSignatureFound, + "No valid signature found. Ensure webhook is configured with a supported signature algorithm " + + "(SHA-256 recommended, SHA-1 for legacy compatibility)."); } } @@ -166,7 +217,7 @@ protected void shouldProvideValidSignature(StaplerRequest req, Object[] args) th * * @return ready-to-hash payload */ - protected String payloadFrom(StaplerRequest req, Object[] args) { + protected String payloadFrom(StaplerRequest2 req, Object[] args) { final String parsedPayload = (String) args[1]; if (req.getContentType().equals(GHEventPayload.PayloadHandler.APPLICATION_JSON)) { diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithm.java b/src/main/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithm.java new file mode 100644 index 000000000..6668f6e81 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithm.java @@ -0,0 +1,98 @@ +package org.jenkinsci.plugins.github.webhook; + +/** + * Enumeration of supported webhook signature algorithms. + * + * @since 1.45.0 + */ +public enum SignatureAlgorithm { + /** + * SHA-256 HMAC signature validation (recommended). + * Uses X-Hub-Signature-256 header with sha256= prefix. + */ + SHA256("sha256", "X-Hub-Signature-256", "HmacSHA256"), + + /** + * SHA-1 HMAC signature validation (legacy). + * Uses X-Hub-Signature header with sha1= prefix. + * + * @deprecated Use SHA256 for enhanced security + */ + @Deprecated + SHA1("sha1", "X-Hub-Signature", "HmacSHA1"); + + private final String prefix; + private final String headerName; + private final String javaAlgorithm; + + /** + * System property to override default signature algorithm. + * Set to "SHA1" to use legacy SHA-1 as default for backwards compatibility. + */ + public static final String DEFAULT_ALGORITHM_PROPERTY = "jenkins.github.webhook.signature.default"; + + /** + * Gets the default algorithm for new configurations. + * Defaults to SHA-256 for security, but can be overridden via system property. + * This is evaluated dynamically to respect system property changes. + * + * @return the default algorithm based on current system property + */ + public static SignatureAlgorithm getDefault() { + return getDefaultAlgorithm(); + } + + SignatureAlgorithm(String prefix, String headerName, String javaAlgorithm) { + this.prefix = prefix; + this.headerName = headerName; + this.javaAlgorithm = javaAlgorithm; + } + + /** + * @return the prefix used in signature strings (e.g. "sha256", "sha1") + */ + public String getPrefix() { + return prefix; + } + + /** + * @return the HTTP header name for this algorithm + */ + public String getHeaderName() { + return headerName; + } + + /** + * @return the Java algorithm name for HMAC computation + */ + public String getJavaAlgorithm() { + return javaAlgorithm; + } + + /** + * @return the expected signature prefix including equals sign (e.g. "sha256=", "sha1=") + */ + public String getSignaturePrefix() { + return prefix + "="; + } + + /** + * Determines the default signature algorithm based on system property. + * Defaults to SHA-256 for security, but allows SHA-1 override for legacy environments. + * + * @return the default algorithm to use + */ + private static SignatureAlgorithm getDefaultAlgorithm() { + String property = System.getProperty(DEFAULT_ALGORITHM_PROPERTY); + if (property == null || property.trim().isEmpty()) { + // No property set, use secure SHA-256 default + return SHA256; + } + try { + return SignatureAlgorithm.valueOf(property.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + // Invalid property value, default to secure SHA-256 + return SHA256; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java index 5db84fa3c..e809c8b05 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/WebhookManager.java @@ -6,7 +6,7 @@ import hudson.model.Item; import hudson.model.Job; import hudson.util.Secret; -import org.apache.commons.lang.Validate; +import org.apache.commons.lang3.Validate; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor; import org.jenkinsci.plugins.github.config.HookSecretConfig; @@ -21,7 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.net.URL; import java.util.Collection; @@ -174,7 +174,7 @@ private GHRepository repoWithWebhookAccess(GitHubRepositoryName name) { com.google.common.base.Optional repoWithAdminAccess = reposAllowedtoManageWebhooks .firstMatch(withAdminAccess()); if (!repoWithAdminAccess.isPresent()) { - LOGGER.debug("None of the github repos configured have admin access for: {}", name); + LOGGER.info("None of the github repos configured have admin access for: {}", name); return null; } GHRepository repo = repoWithAdminAccess.get(); @@ -192,7 +192,7 @@ private GHRepository repoWithWebhookAccess(GitHubRepositoryName name) { protected Function createHookSubscribedTo(final List events) { return new NullSafeFunction() { @Override - protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) { + protected GHHook applyNullSafe(@NonNull GitHubRepositoryName name) { try { GHRepository repo = repoWithWebhookAccess(name); if (repo == null) { @@ -239,7 +239,7 @@ protected GHHook applyNullSafe(@Nonnull GitHubRepositoryName name) { protected Predicate log(final String format) { return new NullSafePredicate() { @Override - protected boolean applyNullSafe(@Nonnull GHHook input) { + protected boolean applyNullSafe(@NonNull GHHook input) { LOGGER.debug(format("%s {} (events: {})", format), input.getUrl(), input.getEvents()); return true; } @@ -254,7 +254,7 @@ protected boolean applyNullSafe(@Nonnull GHHook input) { protected Predicate withAdminAccess() { return new NullSafePredicate() { @Override - protected boolean applyNullSafe(@Nonnull GHRepository repo) { + protected boolean applyNullSafe(@NonNull GHRepository repo) { return repo.hasAdminAccess(); } }; @@ -269,7 +269,7 @@ protected boolean applyNullSafe(@Nonnull GHRepository repo) { */ protected Predicate serviceWebhookFor(final URL url) { return new NullSafePredicate() { - protected boolean applyNullSafe(@Nonnull GHHook hook) { + protected boolean applyNullSafe(@NonNull GHHook hook) { return hook.getName().equals("jenkins") && hook.getConfig().get("jenkins_hook_url").equals(url.toExternalForm()); } @@ -285,7 +285,7 @@ protected boolean applyNullSafe(@Nonnull GHHook hook) { */ protected Predicate webhookFor(final URL url) { return new NullSafePredicate() { - protected boolean applyNullSafe(@Nonnull GHHook hook) { + protected boolean applyNullSafe(@NonNull GHHook hook) { return hook.getName().equals("web") && hook.getConfig().get("url").equals(url.toExternalForm()); } @@ -298,7 +298,7 @@ protected boolean applyNullSafe(@Nonnull GHHook hook) { protected Function> eventsFromHook() { return new NullSafeFunction>() { @Override - protected Iterable applyNullSafe(@Nonnull GHHook input) { + protected Iterable applyNullSafe(@NonNull GHHook input) { return input.getEvents(); } }; @@ -314,7 +314,7 @@ protected Iterable applyNullSafe(@Nonnull GHHook input) { protected Function> fetchHooks() { return new NullSafeFunction>() { @Override - protected List applyNullSafe(@Nonnull GHRepository repo) { + protected List applyNullSafe(@NonNull GHRepository repo) { try { return repo.getHooks(); } catch (IOException e) { @@ -332,7 +332,7 @@ protected List applyNullSafe(@Nonnull GHRepository repo) { */ protected Function createWebhook(final URL url, final Set events) { return new NullSafeFunction() { - protected GHHook applyNullSafe(@Nonnull GHRepository repo) { + protected GHHook applyNullSafe(@NonNull GHRepository repo) { try { final HashMap config = new HashMap<>(); config.put("url", url.toExternalForm()); @@ -359,7 +359,7 @@ protected GHHook applyNullSafe(@Nonnull GHRepository repo) { */ protected Predicate deleteWebhook() { return new NullSafePredicate() { - protected boolean applyNullSafe(@Nonnull GHHook hook) { + protected boolean applyNullSafe(@NonNull GHHook hook) { try { hook.delete(); return true; diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java index 7568af0e9..95180fddb 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventSubscriber.java @@ -73,25 +73,10 @@ protected void onEvent(final GHSubscriberEvent event) { LOGGER.warn("Received malformed PushEvent: " + event.getPayload(), e); return; } - URL repoUrl = push.getRepository().getUrl(); + URL htmlUrl = push.getRepository().getHtmlUrl(); final String pusherName = push.getPusher().getName(); - LOGGER.info("Received PushEvent for {} from {}", repoUrl, event.getOrigin()); - GitHubRepositoryName fromEventRepository = GitHubRepositoryName.create(repoUrl.toExternalForm()); - - if (fromEventRepository == null) { - // On push event on github.com url === html_url - // this is not consistent with the API docs and with hosted repositories - // see https://goo.gl/c1qmY7 - // let's retry with 'html_url' - URL htmlUrl = push.getRepository().getHtmlUrl(); - fromEventRepository = GitHubRepositoryName.create(htmlUrl.toExternalForm()); - if (fromEventRepository != null) { - LOGGER.debug("PushEvent handling: 'html_url' field " - + "has been used to retrieve project information (instead of default 'url' field)"); - } - } - - final GitHubRepositoryName changedRepository = fromEventRepository; + LOGGER.info("Received PushEvent for {} from {}", htmlUrl, event.getOrigin()); + final GitHubRepositoryName changedRepository = GitHubRepositoryName.create(htmlUrl.toExternalForm()); if (changedRepository != null) { // run in high privilege to see all the projects anonymous users don't see. @@ -128,7 +113,7 @@ public void run() { } } else { - LOGGER.warn("Malformed repo url {}", repoUrl); + LOGGER.warn("Malformed repo html url {}", htmlUrl); } } } diff --git a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java index 0d2cbe359..bc7141bf0 100644 --- a/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java +++ b/src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriber.java @@ -6,7 +6,7 @@ import java.io.IOException; import java.io.StringReader; import java.util.Set; -import javax.inject.Inject; +import jakarta.inject.Inject; import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; import org.kohsuke.github.GHEvent; diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy index c9a140f5c..768800958 100644 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy +++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/config.groovy @@ -4,17 +4,14 @@ import com.cloudbees.jenkins.GitHubPushTrigger tr { td(colspan: 4) { - div(id: 'gh-hooks-warn') + def url = descriptor.getCheckMethod('hookRegistered').toCheckUrl() + def input = "input[name='${GitHubPushTrigger.class.getName().replace('.', '-')}']" + + div(id: 'gh-hooks-warn', + 'data-url': url, + 'data-input': input + ) } } script(src:"${rootURL}${h.getResourcePath()}/plugin/github/js/warning.js") -script { - text(""" -InlineWarning.setup({ - id: 'gh-hooks-warn', - url: ${descriptor.getCheckMethod('hookRegistered').toCheckUrl()}, - input: 'input[name="${GitHubPushTrigger.class.getName().replace(".", "-")}"]' -}).start(); -""") -} diff --git a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html index 7a24dd67a..b1d61d307 100644 --- a/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html +++ b/src/main/resources/com/cloudbees/jenkins/GitHubPushTrigger/help.html @@ -1,2 +1,6 @@ -If Jenkins will receive PUSH GitHub hook from repo defined in Git SCM section it -will trigger Git SCM polling logic. So polling logic in fact belongs to Git SCM. +When Jenkins receives a GitHub push hook, GitHub Plugin checks to see +whether the hook came from a GitHub repository which matches the Git repository defined in SCM/Git section of this job. +If they match and this option is enabled, GitHub Plugin triggers a one-time polling on GITScm. +When GITScm polls GitHub, it finds that there is a change and initiates a build. +The last sentence describes the behavior of Git plugin, +thus the polling and initiating the build is not a part of GitHub plugin. diff --git a/src/main/resources/images/symbols/logo-github.svg b/src/main/resources/images/symbols/logo-github.svg new file mode 100644 index 000000000..4c15b0297 --- /dev/null +++ b/src/main/resources/images/symbols/logo-github.svg @@ -0,0 +1 @@ +GitHub diff --git a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties index 7263d17ac..509773102 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/github/Messages.properties @@ -11,3 +11,9 @@ github.trigger.check.method.warning.details=The webhook for repo {0}/{1} on {2} More info can be found on the global configuration page. This message will be dismissed if Jenkins receives \ a PING event from repo webhook or if you add the repo to the ignore list in the global configuration. unknown.error=Unknown error +duplicate.events.administrative.monitor.displayname=GitHub Duplicate Events +duplicate.events.administrative.monitor.description=Warns about duplicate events received from GitHub. +duplicate.events.administrative.monitor.blurb=Duplicate events were received from GitHub, possibly due to \ + misconfiguration (e.g., multiple webhooks targeting the same Jenkins controller at the repository or organization \ + level), potentially causing redundant builds or at least wasted work. \ + Click here to inspect the last tracked duplicate event payload. diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/description.jelly b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/description.jelly new file mode 100644 index 000000000..11cde3e78 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/description.jelly @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/message.jelly b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/message.jelly new file mode 100644 index 000000000..d67740516 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor/message.jelly @@ -0,0 +1,9 @@ + + +

+
+ + + +
+ diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy index fdc8fad55..96077fbb5 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config.groovy @@ -27,10 +27,11 @@ f.section(title: descriptor.displayName) { f.entry(title: _("Override Hook URL")) { g.blockWrapper { f.optionalBlock(title: _("Specify another hook URL for GitHub configuration"), + name: "isOverrideHookUrl", inline: true, checked: instance.isOverrideHookUrl()) { f.entry(field: "hookUrl") { - f.textbox(checkMethod: "post") + f.textbox(checkMethod: "post", name: "hookUrl") } } } diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties index 61a2de581..6ddcfbde4 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubPluginConfig/config_zh_CN.properties @@ -20,14 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -GitHub\ Servers=Github \u670D\u52A1\u5668 -Add\ GitHub\ Server=\u6DFB\u52A0 Github \u670D\u52A1\u5668 +GitHub\ Servers=GitHub \u670D\u52A1\u5668 +Add\ GitHub\ Server=\u6DFB\u52A0 GitHub \u670D\u52A1\u5668 Re-register\ hooks\ for\ all\ jobs=\u7ED9\u6240\u6709\u4EFB\u52A1\u91CD\u65B0\u6CE8\u518C hook Scanning\ all\ items...=\u626B\u63CF\u6240\u6709\u7684\u9879\u76EE... Override\ Hook\ URL=\u8986\u76D6 Hook URL -Specify\ another\ hook\ URL\ for\ GitHub\ configuration=\u4E3A Github \u6307\u5B9A\u53E6\u5916\u4E00\u4E2A Hook URL +Specify\ another\ hook\ URL\ for\ GitHub\ configuration=\u4E3A GitHub \u6307\u5B9A\u53E6\u5916\u4E00\u4E2A Hook URL Additional\ actions=\u9644\u52A0\u52A8\u4F5C -Manage\ additional\ GitHub\ actions=\u7BA1\u7406 Github \u9644\u52A0\u52A8\u4F5C +Manage\ additional\ GitHub\ actions=\u7BA1\u7406 GitHub \u9644\u52A0\u52A8\u4F5C diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties index 0194140d7..6bd83598d 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties +++ b/src/main/resources/org/jenkinsci/plugins/github/config/GitHubServerConfig/config_zh_CN.properties @@ -25,4 +25,4 @@ Credentials=\u51ED\u636E Test\ connection=\u8FDE\u63A5\u6D4B\u8BD5 Testing...=\u6D4B\u8BD5\u4E2D... Manage\ hooks=\u7BA1\u7406 Hook -GitHub\ client\ cache\ size\ (MB)=Github \u5BA2\u6237\u7AEF\u7F13\u5B58(MB) +GitHub\ client\ cache\ size\ (MB)=GitHub \u5BA2\u6237\u7AEF\u7F13\u5B58(MB) diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy index 85e11ffae..2e5cce9ff 100644 --- a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy +++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/config.groovy @@ -6,3 +6,7 @@ def c = namespace(lib.CredentialsTagLib); f.entry(title: _("Shared secret"), field: "credentialsId", help: descriptor.getHelpFile('sharedSecret')) { c.select(context: app, includeUser: false, expressionAllowed: false) } + +f.entry(title: _("Signature algorithm"), field: "signatureAlgorithm") { + f.select() +} diff --git a/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-signatureAlgorithm.html b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-signatureAlgorithm.html new file mode 100644 index 000000000..5092fb6d9 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github/config/HookSecretConfig/help-signatureAlgorithm.html @@ -0,0 +1,13 @@ +
+

Choose the signature algorithm for webhook validation:

+
    +
  • SHA-256 (Recommended): Modern, secure HMAC signature validation using the + X-Hub-Signature-256 header. This is GitHub's recommended approach for enhanced security.
  • +
  • SHA-1 (Legacy): Legacy HMAC signature validation using the + X-Hub-Signature header. Only use this for existing webhooks during migration period.
  • +
+

Note: When changing algorithms, ensure your GitHub webhook configuration uses the corresponding + signature header (X-Hub-Signature-256 for SHA-256 or X-Hub-Signature for SHA-1).

+

System Property Override: The default algorithm can be overridden using the system property + -Djenkins.github.webhook.signature.default=SHA1 for backwards compatibility with legacy CI environments.

+
\ No newline at end of file diff --git a/src/main/webapp/img/logo.svg b/src/main/webapp/img/logo.svg deleted file mode 100644 index 15a33d1ae..000000000 --- a/src/main/webapp/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/main/webapp/js/warning.js b/src/main/webapp/js/warning.js index 8bb1198dd..994242240 100644 --- a/src/main/webapp/js/warning.js +++ b/src/main/webapp/js/warning.js @@ -9,24 +9,64 @@ var InlineWarning = (function () { exports.setup = function (opts) { options = opts; + + // Check if the URL needs concatenation + if (opts.url.includes("'+'")) { + // Manually concatenate the parts + let parts = opts.url.split("'+'"); + options.url = parts.map(part => part.replace(/'/g, '')).join(''); + } else { + options.url = opts.url; + } + return exports; }; exports.start = function () { // Ignore when GH trigger unchecked - if (!$$(options.input).first().checked) { + if (!document.querySelector(options.input).checked) { return; } - new Ajax.PeriodicalUpdater( - options.id, - options.url, - { - method: 'get', - frequency: 10, - decay: 2 - } - ); + var frequency = 10; + var decay = 2; + var lastResponseText; + var fetchData = function () { + fetch(options.url).then((rsp) => { + rsp.text().then((responseText) => { + if (responseText !== lastResponseText) { + document.getElementById(options.id).innerHTML = responseText; + lastResponseText = responseText; + frequency = 10; + } else { + frequency *= decay; + } + setTimeout(fetchData, frequency * 1000); + }); + }); + }; + fetchData(); }; return exports; -})(); \ No newline at end of file +})(); + +document.addEventListener('DOMContentLoaded', function() { + var warningElement = document.getElementById('gh-hooks-warn'); + + if (warningElement) { + var url = warningElement.getAttribute('data-url'); + var input = warningElement.getAttribute('data-input'); + + if (url && input) { + InlineWarning.setup({ + id: 'gh-hooks-warn', + url: url, + input: input + }).start(); + } else { + console.error('URL or Input is null'); + } + } else { + console.error('Element with ID "gh-hooks-warn" not found'); + } +}); \ No newline at end of file diff --git a/src/main/webapp/logov3.png b/src/main/webapp/logov3.png deleted file mode 100644 index 7ef7d59b1..000000000 Binary files a/src/main/webapp/logov3.png and /dev/null differ diff --git a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java index 50f167f6b..7ea4c3ef3 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubCommitNotifierTest.java @@ -1,7 +1,7 @@ package com.cloudbees.jenkins; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.Build; @@ -13,24 +13,22 @@ import hudson.plugins.git.Revision; import hudson.plugins.git.util.BuildData; import hudson.util.VersionNumber; +import jakarta.inject.Inject; import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; -import org.jenkinsci.plugins.github.test.GHMockRule; -import org.jenkinsci.plugins.github.test.GHMockRule.FixedGHRepoNameTestContributor; -import org.jenkinsci.plugins.github.test.InjectJenkinsMembersRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExternalResource; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; +import org.jenkinsci.plugins.github.test.GitHubMockExtension; +import org.jenkinsci.plugins.github.test.GitHubMockExtension.FixedGHRepoNameTestContributor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestBuilder; import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - -import javax.inject.Inject; +import org.mockito.junit.jupiter.MockitoExtension; import static com.cloudbees.jenkins.GitHubSetCommitStatusBuilderTest.SOME_SHA; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; @@ -43,48 +41,44 @@ /** * Tests for {@link GitHubCommitNotifier}. * - * @author Oleg Nenashev + * @author Oleg Nenashev */ -@RunWith(MockitoJUnitRunner.class) +@WithJenkins +@ExtendWith(MockitoExtension.class) public class GitHubCommitNotifierTest { - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) public BuildData data; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) public Revision rev; @Inject public GitHubPluginConfig config; - public JenkinsRule jRule = new JenkinsRule(); - - @Rule - public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); + private JenkinsRule jRule; - @Rule - public GHMockRule github = new GHMockRule( - new WireMockRule( - wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)) - )) + @RegisterExtension + static GitHubMockExtension github = new GitHubMockExtension(WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)))) .stubUser() .stubRepo() .stubStatuses(); - @Rule - public ExternalResource prep = new ExternalResource() { - @Override - protected void before() throws Throwable { - when(data.getLastBuiltRevision()).thenReturn(rev); - data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); - when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); - } - }; + @BeforeEach + void before(JenkinsRule rule) throws Throwable { + jRule = rule; + jRule.getInstance().getInjector().injectMembers(this); + + when(data.getLastBuiltRevision()).thenReturn(rev); + data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); + when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); + } @Test @Issue("JENKINS-23641") - public void testNoBuildData() throws Exception { + void testNoBuildData() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject("23641_noBuildData"); prj.getPublishersList().add(new GitHubCommitNotifier()); Build b = prj.scheduleBuild2(0).get(); @@ -94,7 +88,7 @@ public void testNoBuildData() throws Exception { @Test @Issue("JENKINS-23641") - public void testNoBuildRevision() throws Exception { + void testNoBuildRevision() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject(); prj.setScm(new GitSCM("http://non.existent.git.repo.nowhere/repo.git")); prj.getPublishersList().add(new GitHubCommitNotifier()); @@ -106,7 +100,7 @@ public void testNoBuildRevision() throws Exception { @Test @Issue("JENKINS-25312") - public void testMarkUnstableOnCommitNotifierFailure() throws Exception { + void testMarkUnstableOnCommitNotifierFailure() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject(); prj.getPublishersList().add(new GitHubCommitNotifier(Result.UNSTABLE.toString())); Build b = prj.scheduleBuild2(0).get(); @@ -115,7 +109,7 @@ public void testMarkUnstableOnCommitNotifierFailure() throws Exception { @Test @Issue("JENKINS-25312") - public void testMarkSuccessOnCommitNotifierFailure() throws Exception { + void testMarkSuccessOnCommitNotifierFailure() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject(); prj.getPublishersList().add(new GitHubCommitNotifier(Result.SUCCESS.toString())); Build b = prj.scheduleBuild2(0).get(); @@ -123,7 +117,7 @@ public void testMarkSuccessOnCommitNotifierFailure() throws Exception { } @Test - public void shouldWriteStatusOnGH() throws Exception { + void shouldWriteStatusOnGH() throws Exception { config.getConfigs().add(github.serverConfig()); FreeStyleProject prj = jRule.createFreeStyleProject(); @@ -139,7 +133,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen prj.scheduleBuild2(0).get(); - github.service().verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); + github.verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); } private Build safelyGenerateBuild(FreeStyleProject prj) throws InterruptedException, java.util.concurrent.ExecutionException { diff --git a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java index 00a529c28..cf301eb75 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubPushTriggerTest.java @@ -5,19 +5,19 @@ import hudson.plugins.git.util.Build; import hudson.plugins.git.util.BuildData; import hudson.util.FormValidation; +import jakarta.inject.Inject; import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.admin.GitHubHookRegisterProblemMonitor; import org.jenkinsci.plugins.github.webhook.subscriber.DefaultPushGHEventListenerTest; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import javax.inject.Inject; import java.io.IOException; import java.util.HashMap; import java.util.concurrent.TimeUnit; @@ -30,7 +30,8 @@ /** * @author lanwen (Merkushev Kirill) */ -public class GitHubPushTriggerTest { +@WithJenkins +class GitHubPushTriggerTest { private static final GitHubRepositoryName REPO = new GitHubRepositoryName("host", "user", "repo"); private static final GitSCM REPO_GIT_SCM = new GitSCM("git://host/user/repo.git"); @@ -40,11 +41,11 @@ public class GitHubPushTriggerTest { @Inject private GitHubPushTrigger.DescriptorImpl descriptor; - @Rule - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jRule = rule; jRule.getInstance().getInjector().injectMembers(this); } @@ -53,7 +54,7 @@ public void setUp() throws Exception { */ @Test @Issue("JENKINS-27136") - public void shouldStartWorkflowByTrigger() throws Exception { + void shouldStartWorkflowByTrigger() throws Exception { WorkflowJob job = jRule.getInstance().createProject(WorkflowJob.class, "test-workflow-job"); GitHubPushTrigger trigger = new GitHubPushTrigger(); trigger.start(job, false); @@ -79,7 +80,7 @@ public void shouldStartWorkflowByTrigger() throws Exception { @Test @Issue("JENKINS-24690") - public void shouldReturnWaringOnHookProblem() throws Exception { + void shouldReturnWaringOnHookProblem() throws Exception { monitor.registerProblem(REPO, new IOException()); FreeStyleProject job = jRule.createFreeStyleProject(); job.setScm(REPO_GIT_SCM); @@ -89,7 +90,7 @@ public void shouldReturnWaringOnHookProblem() throws Exception { } @Test - public void shouldReturnOkOnNoAnyProblem() throws Exception { + void shouldReturnOkOnNoAnyProblem() throws Exception { FreeStyleProject job = jRule.createFreeStyleProject(); job.setScm(REPO_GIT_SCM); diff --git a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java index 7e03528b7..20f0e75dd 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubSetCommitStatusBuilderTest.java @@ -1,7 +1,7 @@ package com.cloudbees.jenkins; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.Build; @@ -11,26 +11,25 @@ import hudson.plugins.git.Revision; import hudson.plugins.git.util.BuildData; import hudson.tasks.Builder; +import jakarta.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; -import org.jenkinsci.plugins.github.test.GHMockRule; -import org.jenkinsci.plugins.github.test.GHMockRule.FixedGHRepoNameTestContributor; -import org.jenkinsci.plugins.github.test.InjectJenkinsMembersRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExternalResource; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; +import org.jenkinsci.plugins.github.test.GitHubMockExtension; +import org.jenkinsci.plugins.github.test.GitHubMockExtension.FixedGHRepoNameTestContributor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestBuilder; import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.jvnet.hudson.test.recipes.LocalData; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -import javax.inject.Inject; import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; @@ -42,49 +41,45 @@ /** * Tests for {@link GitHubSetCommitStatusBuilder}. * - * @author Oleg Nenashev + * @author Oleg Nenashev */ -@RunWith(MockitoJUnitRunner.class) +@WithJenkins +@ExtendWith(MockitoExtension.class) public class GitHubSetCommitStatusBuilderTest { public static final String SOME_SHA = StringUtils.repeat("f", 40); - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) public BuildData data; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) public Revision rev; @Inject public GitHubPluginConfig config; - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; - @Rule - public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); - - @Rule - public GHMockRule github = new GHMockRule( - new WireMockRule( - wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)) - )) + @RegisterExtension + static GitHubMockExtension github = new GitHubMockExtension(WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)))) .stubUser() .stubRepo() .stubStatuses(); - @Rule - public ExternalResource prep = new ExternalResource() { - @Override - protected void before() throws Throwable { - when(data.getLastBuiltRevision()).thenReturn(rev); - data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); - when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); - } - }; + @BeforeEach + void before(JenkinsRule rule) throws Throwable { + jRule = rule; + jRule.getInstance().getInjector().injectMembers(this); + + when(data.getLastBuiltRevision()).thenReturn(rev); + data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); + when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); + } @Test @Issue("JENKINS-23641") - public void shouldIgnoreIfNoBuildData() throws Exception { + void shouldIgnoreIfNoBuildData() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject("23641_noBuildData"); prj.getBuildersList().add(new GitHubSetCommitStatusBuilder()); Build b = prj.scheduleBuild2(0).get(); @@ -94,7 +89,7 @@ public void shouldIgnoreIfNoBuildData() throws Exception { @Test @LocalData @Issue("JENKINS-32132") - public void shouldLoadNullStatusMessage() throws Exception { + void shouldLoadNullStatusMessage() throws Exception { config.getConfigs().add(github.serverConfig()); FreeStyleProject prj = jRule.getInstance().getItemByFullName("step", FreeStyleProject.class); @@ -110,7 +105,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen prj.getBuildersList().replaceBy(builders); prj.scheduleBuild2(0).get(); - github.service().verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); + github.verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); } @TestExtension diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusionTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusionTest.java index fcf8317e1..bd23444d6 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusionTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookCrumbExclusionTest.java @@ -1,29 +1,28 @@ package com.cloudbees.jenkins; -import org.junit.Before; -import org.junit.Test; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import javax.servlet.FilterChain; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.TestCase.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class GitHubWebHookCrumbExclusionTest { +class GitHubWebHookCrumbExclusionTest { private GitHubWebHookCrumbExclusion exclusion; private HttpServletRequest req; private HttpServletResponse resp; private FilterChain chain; - @Before - public void before() { + @BeforeEach + void before() { exclusion = new GitHubWebHookCrumbExclusion(); req = mock(HttpServletRequest.class); resp = mock(HttpServletResponse.class); @@ -31,35 +30,35 @@ public void before() { } @Test - public void testFullPath() throws Exception { + void testFullPath() throws Exception { when(req.getPathInfo()).thenReturn("/github-webhook/"); assertTrue(exclusion.process(req, resp, chain)); verify(chain, times(1)).doFilter(req, resp); } @Test - public void testFullPathWithoutSlash() throws Exception { + void testFullPathWithoutSlash() throws Exception { when(req.getPathInfo()).thenReturn("/github-webhook"); assertTrue(exclusion.process(req, resp, chain)); verify(chain, times(1)).doFilter(req, resp); } @Test - public void testInvalidPath() throws Exception { + void testInvalidPath() throws Exception { when(req.getPathInfo()).thenReturn("/some-other-url/"); assertFalse(exclusion.process(req, resp, chain)); verify(chain, never()).doFilter(req, resp); } @Test - public void testNullPath() throws Exception { + void testNullPath() throws Exception { when(req.getPathInfo()).thenReturn(null); assertFalse(exclusion.process(req, resp, chain)); verify(chain, never()).doFilter(req, resp); } @Test - public void testEmptyPath() throws Exception { + void testEmptyPath() throws Exception { when(req.getPathInfo()).thenReturn(""); assertFalse(exclusion.process(req, resp, chain)); verify(chain, never()).doFilter(req, resp); diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java index d3db9ad76..7c66858f3 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookFullTest.java @@ -5,38 +5,38 @@ import io.restassured.builder.RequestSpecBuilder; import io.restassured.http.Header; import io.restassured.specification.RequestSpecification; +import jakarta.inject.Inject; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; import org.jenkinsci.plugins.github.webhook.GHEventHeader; import org.jenkinsci.plugins.github.webhook.GHEventPayload; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExternalResource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GHEvent; -import javax.inject.Inject; import java.io.File; import java.io.IOException; import static io.restassured.RestAssured.given; import static io.restassured.config.EncoderConfig.encoderConfig; import static io.restassured.config.RestAssuredConfig.newConfig; +import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static jakarta.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; import static java.lang.String.format; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; -import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.apache.commons.lang3.ClassUtils.PACKAGE_SEPARATOR; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.notNullValue; import static org.jenkinsci.plugins.github.test.HookSecretHelper.removeSecretIn; import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecretIn; -import static org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER; +import static org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload.Processor.*; /** * @author lanwen (Merkushev Kirill) */ +@WithJenkins public class GitHubWebHookFullTest { // GitHub doesn't send the charset per docs, so re-use the exact content-type from the handler @@ -48,37 +48,28 @@ public class GitHubWebHookFullTest { public static final String NOT_NULL_VALUE = "nonnull"; private RequestSpecification spec; - + @Inject private GitHubPluginConfig config; - @ClassRule - public static JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; - @Rule - public ExternalResource inject = new ExternalResource() { - @Override - protected void before() throws Throwable { - jenkins.getInstance().getInjector().injectMembers(GitHubWebHookFullTest.this); - } - }; - - @Rule - public ExternalResource setup = new ExternalResource() { - @Override - protected void before() throws Throwable { - spec = new RequestSpecBuilder() - .setConfig(newConfig() - .encoderConfig(encoderConfig() - .defaultContentCharset(Charsets.UTF_8.name()) - // GitHub doesn't add charsets, so don't test with them - .appendDefaultContentCharsetToContentTypeIfUndefined(false))) - .build(); - } - }; + @BeforeEach + void before(JenkinsRule rule) throws Throwable { + jenkins = rule; + jenkins.getInstance().getInjector().injectMembers(this); + + spec = new RequestSpecBuilder() + .setConfig(newConfig() + .encoderConfig(encoderConfig() + .defaultContentCharset(Charsets.UTF_8.name()) + // GitHub doesn't add charsets, so don't test with them + .appendDefaultContentCharsetToContentTypeIfUndefined(false))) + .build(); + } @Test - public void shouldParseJsonWebHookFromGH() throws Exception { + void shouldParseJsonWebHookFromGH() throws Exception { removeSecretIn(config); given().spec(spec) .header(eventHeader(GHEvent.PUSH)) @@ -90,22 +81,24 @@ public void shouldParseJsonWebHookFromGH() throws Exception { @Test - public void shouldParseJsonWebHookFromGHWithSignHeader() throws Exception { + void shouldParseJsonWebHookFromGHWithSignHeader() throws Exception { String hash = "355e155fc3d10c4e5f2c6086a01281d2e947d932"; + String hash256 = "85e61999573c7023720a12375e1e55d18a0870e1ef880736f6ffc9273d0519e3"; String secret = "123"; - + storeSecretIn(config, secret); given().spec(spec) .header(eventHeader(GHEvent.PUSH)) .header(JSON_CONTENT_TYPE) .header(SIGNATURE_HEADER, format("sha1=%s", hash)) + .header(SIGNATURE_HEADER_SHA256, format("%s%s", SHA256_PREFIX, hash256)) .body(classpath(String.format("payloads/ping_hash_%s_secret_%s.json", hash, secret))) .log().all() .expect().log().all().statusCode(SC_OK).request().post(getPath()); } @Test - public void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { + void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { given().spec(spec) .header(eventHeader(GHEvent.PUSH)) .header(FORM_CONTENT_TYPE) @@ -115,7 +108,7 @@ public void shouldParseFormWebHookOrServiceHookFromGH() throws Exception { } @Test - public void shouldParsePingFromGH() throws Exception { + void shouldParsePingFromGH() throws Exception { given().spec(spec) .header(eventHeader(GHEvent.PING)) .header(JSON_CONTENT_TYPE) @@ -128,7 +121,7 @@ public void shouldParsePingFromGH() throws Exception { } @Test - public void shouldReturnErrOnEmptyPayloadAndHeader() throws Exception { + void shouldReturnErrOnEmptyPayloadAndHeader() throws Exception { given().spec(spec) .log().all() .expect().log().all() @@ -139,7 +132,7 @@ public void shouldReturnErrOnEmptyPayloadAndHeader() throws Exception { } @Test - public void shouldReturnErrOnEmptyPayload() throws Exception { + void shouldReturnErrOnEmptyPayload() throws Exception { given().spec(spec) .header(eventHeader(GHEvent.PUSH)) .log().all() @@ -151,7 +144,7 @@ public void shouldReturnErrOnEmptyPayload() throws Exception { } @Test - public void shouldReturnErrOnGetReq() throws Exception { + void shouldReturnErrOnGetReq() throws Exception { given().spec(spec) .log().all().expect().log().all() .statusCode(SC_METHOD_NOT_ALLOWED) @@ -160,7 +153,7 @@ public void shouldReturnErrOnGetReq() throws Exception { } @Test - public void shouldProcessSelfTest() throws Exception { + void shouldProcessSelfTest() throws Exception { given().spec(spec) .header(new Header(GitHubWebHook.URL_VALIDATION_HEADER, NOT_NULL_VALUE)) .log().all() @@ -192,7 +185,7 @@ public static String classpath(Class clazz, String path) { throw new RuntimeException(format("Can't load %s for class %s", path, clazz), e); } } - + private String getPath(){ return jenkins.getInstance().getRootUrl() + GitHubWebHook.URLNAME.concat("/"); } diff --git a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java index 0f1c367e9..0c5fa30d3 100644 --- a/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java +++ b/src/test/java/com/cloudbees/jenkins/GitHubWebHookTest.java @@ -1,36 +1,40 @@ package com.cloudbees.jenkins; import com.google.inject.Inject; - import hudson.model.Item; -import hudson.model.Job; - import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GHEvent; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest2; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import java.util.Set; import static com.google.common.collect.Sets.immutableEnumSet; import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; /** * @author lanwen (Merkushev Kirill) */ -public class GitHubWebHookTest { +@WithJenkins +@ExtendWith(MockitoExtension.class) +class GitHubWebHookTest { public static final String PAYLOAD = "{}"; - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; @Inject private IssueSubscriber subscriber; @@ -41,31 +45,44 @@ public class GitHubWebHookTest { @Inject private ThrowablePullRequestSubscriber throwablePullRequestSubscriber; - @Before - public void setUp() throws Exception { + @Mock + private StaplerRequest2 req2; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; jenkins.getInstance().getInjector().injectMembers(this); } @Test - public void shouldCallExtensionInterestedInIssues() throws Exception { - new GitHubWebHook().doIndex(GHEvent.ISSUES, PAYLOAD); - assertThat("should get interested event", subscriber.lastEvent(), equalTo(GHEvent.ISSUES)); + void shouldCallExtensionInterestedInIssues() throws Exception { + try (var mockedStapler = Mockito.mockStatic(Stapler.class)) { + mockedStapler.when(Stapler::getCurrentRequest2).thenReturn(req2); + new GitHubWebHook().doIndex(GHEvent.ISSUES, PAYLOAD); + assertThat("should get interested event", subscriber.lastEvent(), equalTo(GHEvent.ISSUES)); + } } @Test - public void shouldNotCallAnyExtensionsWithPublicEventIfNotRegistered() throws Exception { - new GitHubWebHook().doIndex(GHEvent.PUBLIC, PAYLOAD); - assertThat("should not get not interested event", subscriber.lastEvent(), nullValue()); + void shouldNotCallAnyExtensionsWithPublicEventIfNotRegistered() throws Exception { + try (var mockedStapler = Mockito.mockStatic(Stapler.class)) { + mockedStapler.when(Stapler::getCurrentRequest2).thenReturn(req2); + new GitHubWebHook().doIndex(GHEvent.PUBLIC, PAYLOAD); + assertThat("should not get not interested event", subscriber.lastEvent(), nullValue()); + } } @Test - public void shouldCatchThrowableOnFailedSubscriber() throws Exception { - new GitHubWebHook().doIndex(GHEvent.PULL_REQUEST, PAYLOAD); - assertThat("each extension should get event", - asList( - pullRequestSubscriber.lastEvent(), - throwablePullRequestSubscriber.lastEvent() - ), everyItem(equalTo(GHEvent.PULL_REQUEST))); + void shouldCatchThrowableOnFailedSubscriber() throws Exception { + try (var mockedStapler = Mockito.mockStatic(Stapler.class)) { + mockedStapler.when(Stapler::getCurrentRequest2).thenReturn(req2); + new GitHubWebHook().doIndex(GHEvent.PULL_REQUEST, PAYLOAD); + assertThat("each extension should get event", + asList( + pullRequestSubscriber.lastEvent(), + throwablePullRequestSubscriber.lastEvent() + ), everyItem(equalTo(GHEvent.PULL_REQUEST))); + } } @TestExtension @@ -103,7 +120,7 @@ protected void onEvent(GHEvent event, String payload) { public static class TestSubscriber extends GHEventsSubscriber { - private GHEvent interested; + private final GHEvent interested; private GHEvent event; public TestSubscriber(GHEvent interested) { diff --git a/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java b/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java index c1c313f3b..0a41f5d6c 100644 --- a/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java +++ b/src/test/java/com/cloudbees/jenkins/GlobalConfigSubmitTest.java @@ -1,12 +1,12 @@ package com.cloudbees.jenkins; -import com.gargoylesoftware.htmlunit.html.HtmlForm; -import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; import org.jenkinsci.plugins.github.GitHubPlugin; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.xml.sax.SAXException; import java.io.IOException; @@ -14,62 +14,53 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; /** * Test Class for {@link GitHubPushTrigger}. * * @author Seiji Sogabe */ -@Ignore("Have troubles with memory consumption") -public class GlobalConfigSubmitTest { +@WithJenkins +class GlobalConfigSubmitTest { - public static final String OVERRIDE_HOOK_URL_CHECKBOX = "_.isOverrideHookUrl"; - public static final String HOOK_URL_INPUT = "_.hookUrl"; + private static final String OVERRIDE_HOOK_URL_CHECKBOX = "isOverrideHookUrl"; + private static final String HOOK_URL_INPUT = "hookUrl"; private static final String WEBHOOK_URL = "http://jenkinsci.example.com/jenkins/github-webhook/"; - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; - @Test - public void shouldSetHookUrl() throws Exception { - HtmlForm form = globalConfig(); - - form.getInputByName(OVERRIDE_HOOK_URL_CHECKBOX).setChecked(true); - form.getInputByName(HOOK_URL_INPUT).setValueAttribute(WEBHOOK_URL); - jenkins.submit(form); - - assertThat(GitHubPlugin.configuration().getHookUrl(), equalTo(new URL(WEBHOOK_URL))); + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; } @Test - public void shouldNotSetHookUrl() throws Exception { - GitHubPlugin.configuration().setHookUrl(WEBHOOK_URL); - + void shouldSetHookUrl() throws Exception { HtmlForm form = globalConfig(); - form.getInputByName(OVERRIDE_HOOK_URL_CHECKBOX).setChecked(false); - form.getInputByName(HOOK_URL_INPUT).setValueAttribute("http://foo"); + form.getInputByName(OVERRIDE_HOOK_URL_CHECKBOX).setChecked(true); + form.getInputByName(HOOK_URL_INPUT).setValue(WEBHOOK_URL); jenkins.submit(form); assertThat(GitHubPlugin.configuration().getHookUrl(), equalTo(new URL(WEBHOOK_URL))); } @Test - public void shouldNotOverrideAPreviousHookUrlIfNotChecked() throws Exception { + void shouldResetHookUrlIfNotChecked() throws Exception { GitHubPlugin.configuration().setHookUrl(WEBHOOK_URL); HtmlForm form = globalConfig(); form.getInputByName(OVERRIDE_HOOK_URL_CHECKBOX).setChecked(false); - form.getInputByName(HOOK_URL_INPUT).setValueAttribute(""); + form.getInputByName(HOOK_URL_INPUT).setValue("http://foo"); jenkins.submit(form); - assertThat(GitHubPlugin.configuration().getHookUrl(), equalTo(new URL(WEBHOOK_URL))); + assertThat(GitHubPlugin.configuration().getHookUrl().toString(), startsWith(jenkins.jenkins.getRootUrl())); } - public HtmlForm globalConfig() throws IOException, SAXException { + private HtmlForm globalConfig() throws IOException, SAXException { JenkinsRule.WebClient client = configureWebClient(); HtmlPage p = client.goTo("configure"); return p.getFormByName("config"); diff --git a/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java b/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java index 274ca74e8..00fd8fbc7 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GitHubRepositoryNameTest.java @@ -1,13 +1,15 @@ package com.coravy.hudson.plugins.github; import com.cloudbees.jenkins.GitHubRepositoryName; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; import org.apache.commons.lang3.StringUtils; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import static com.cloudbees.jenkins.GitHubRepositoryName.create; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -15,59 +17,72 @@ import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withHost; import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withRepoName; import static org.jenkinsci.plugins.github.test.GitHubRepoNameMatchers.withUserName; -import static org.junit.Assert.assertThat; /** * Unit tests of {@link GitHubRepositoryName} */ -@RunWith(DataProviderRunner.class) public class GitHubRepositoryNameTest { public static final String FULL_REPO_NAME = "jenkins/jenkins"; public static final String VALID_HTTPS_GH_PROJECT = "https://github.com/" + FULL_REPO_NAME; - @Test - @DataProvider({ - "git@github.com:jenkinsci/jenkins.git/, github.com, jenkinsci, jenkins", - "git@github.com:jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "git@github.com:jenkinsci/jenkins/, github.com, jenkinsci, jenkins", - "git@github.com:jenkinsci/jenkins, github.com, jenkinsci, jenkins", - "git@gh.company.com:jenkinsci/jenkins.git/, gh.company.com, jenkinsci, jenkins", - "git@gh.company.com:jenkinsci/jenkins.git, gh.company.com, jenkinsci, jenkins", - "git@gh.company.com:jenkinsci/jenkins, gh.company.com, jenkinsci, jenkins", - "git@gh.company.com:jenkinsci/jenkins/, gh.company.com, jenkinsci, jenkins", - "git://github.com/jenkinsci/jenkins.git/, github.com, jenkinsci, jenkins", - "git://github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "git://github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", - "git://github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", - "https://user@github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "https://user@github.com/jenkinsci/jenkins.git/, github.com, jenkinsci, jenkins", - "https://user@github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", - "https://user@github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", - "https://employee@gh.company.com/jenkinsci/jenkins.git/, gh.company.com, jenkinsci, jenkins", - "https://employee@gh.company.com/jenkinsci/jenkins.git, gh.company.com, jenkinsci, jenkins", - "https://employee@gh.company.com/jenkinsci/jenkins, gh.company.com, jenkinsci, jenkins", - "https://employee@gh.company.com/jenkinsci/jenkins/, gh.company.com, jenkinsci, jenkins", - "git://company.net/jenkinsci/jenkins.git/, company.net, jenkinsci, jenkins", - "git://company.net/jenkinsci/jenkins.git, company.net, jenkinsci, jenkins", - "git://company.net/jenkinsci/jenkins, company.net, jenkinsci, jenkins", - "git://company.net/jenkinsci/jenkins/, company.net, jenkinsci, jenkins", - "https://github.com/jenkinsci/jenkins.git/, github.com, jenkinsci, jenkins", - "https://github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "https://github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", - "https://github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", - "ssh://git@github.com/jenkinsci/jenkins.git/, github.com, jenkinsci, jenkins", - "ssh://git@github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "ssh://git@github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", - "ssh://git@github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", - "ssh://github.com/jenkinsci/jenkins.git/, github.com, jenkinsci, jenkins", - "ssh://github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "ssh://github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", - "ssh://github.com/jenkinsci/jenkins/, github.com, jenkinsci, jenkins", - "git+ssh://git@github.com/jenkinsci/jenkins.git, github.com, jenkinsci, jenkins", - "git+ssh://github.com/jenkinsci/jenkins, github.com, jenkinsci, jenkins", - }) - public void githubFullRepo(String url, String host, String user, String repo) { + public static Object[][] repos() { + return new Object[][]{ + new Object[]{"git@github.com:jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git@github.com:jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git@github.com:jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git@github.com:jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"org-12345@github.com:jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"org-12345@github.com:jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"org-12345@github.com:jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"org-12345@github.com:jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"org-12345@gh.company.com:jenkinsci/jenkins.git/", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"git@gh.company.com:jenkinsci/jenkins.git", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"git@gh.company.com:jenkinsci/jenkins", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"git@gh.company.com:jenkinsci/jenkins/", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"git://github.com/jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git://github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git://github.com/jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git://github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://user@github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://user@github.com/jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://user@github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://user@github.com/jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://employee@gh.company.com/jenkinsci/jenkins.git/", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"https://employee@gh.company.com/jenkinsci/jenkins.git", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"https://employee@gh.company.com/jenkinsci/jenkins", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"https://employee@gh.company.com/jenkinsci/jenkins/", "gh.company.com", "jenkinsci", "jenkins"}, + new Object[]{"git://company.net/jenkinsci/jenkins.git/", "company.net", "jenkinsci", "jenkins"}, + new Object[]{"git://company.net/jenkinsci/jenkins.git", "company.net", "jenkinsci", "jenkins"}, + new Object[]{"git://company.net/jenkinsci/jenkins", "company.net", "jenkinsci", "jenkins"}, + new Object[]{"git://company.net/jenkinsci/jenkins/", "company.net", "jenkinsci", "jenkins"}, + new Object[]{"https://github.com/jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"https://github.com/jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://git@github.com/jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://git@github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://git@github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins", + new Object[]{"ssh://git@github.com/jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins", + new Object[]{"ssh://org-12345@github.com/jenkinsci/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://org-12345@github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://org-12345@github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://org-12345@github.com/jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://github.com/jenkinscRi/jenkins.git/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"ssh://github.com/jenkinsci/jenkins/", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git+ssh://git@github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git+ssh://org-12345@github.com/jenkinsci/jenkins.git", "github.com", "jenkinsci", "jenkins"}, + new Object[]{"git+ssh://github.com/jenkinsci/jenkins", "github.com", "jenkinsci", "jenkins"} + } + } + }; + } + + @ParameterizedTest + @MethodSource("repos") + void githubFullRepo(String url, String host, String user, String repo) { assertThat(url, repo(allOf( withHost(host), withUserName(user), @@ -76,7 +91,7 @@ public void githubFullRepo(String url, String host, String user, String repo) { } @Test - public void trimWhitespace() { + void trimWhitespace() { assertThat(" https://user@github.com/jenkinsci/jenkins/ ", repo(allOf( withHost("github.com"), withUserName("jenkinsci"), @@ -84,35 +99,33 @@ public void trimWhitespace() { ))); } - @Test - @DataProvider(value = { - "gopher://gopher.floodgap.com", + @ParameterizedTest + @ValueSource(strings = {"gopher://gopher.floodgap.com", "https//github.com/jenkinsci/jenkins", - "", - "null" - }, trimValues = false) - public void badUrl(String url) { + ""}) + @NullSource + void badUrl(String url) { assertThat(url, repo(nullValue(GitHubRepositoryName.class))); } @Test - public void shouldCreateFromProjectProp() { + void shouldCreateFromProjectProp() { assertThat("project prop vs direct", create(new GithubProjectProperty(VALID_HTTPS_GH_PROJECT)), equalTo(create(VALID_HTTPS_GH_PROJECT))); } @Test - public void shouldIgnoreNull() { + void shouldIgnoreNull() { assertThat("null project prop", create((GithubProjectProperty) null), nullValue()); } @Test - public void shouldIgnoreNullValueOfPP() { + void shouldIgnoreNullValueOfPP() { assertThat("null project prop", create(new GithubProjectProperty(null)), nullValue()); } @Test - public void shouldIgnoreBadValueOfPP() { + void shouldIgnoreBadValueOfPP() { assertThat("null project prop", create(new GithubProjectProperty(StringUtils.EMPTY)), nullValue()); } } diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java index cef4e8bfa..b616ad756 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkActionFactoryTest.java @@ -1,30 +1,34 @@ package com.coravy.hudson.plugins.github; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.empty; -import static org.junit.Assert.assertThat; - -import java.io.IOException; -import java.util.Collection; - +import com.coravy.hudson.plugins.github.GithubLinkAction.GithubLinkActionFactory; +import hudson.model.Action; import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import com.coravy.hudson.plugins.github.GithubLinkAction.GithubLinkActionFactory; +import java.io.IOException; +import java.util.Collection; -import hudson.model.Action; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; -public class GithubLinkActionFactoryTest { - @Rule - public final JenkinsRule rule = new JenkinsRule(); +@WithJenkins +class GithubLinkActionFactoryTest { + private JenkinsRule rule; private final GithubLinkActionFactory factory = new GithubLinkActionFactory(); private static final String PROJECT_URL = "https://github.com/jenkinsci/github-plugin/"; + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + this.rule = rule; + } + private WorkflowJob createExampleJob() throws IOException { return rule.getInstance().createProject(WorkflowJob.class, "example"); } @@ -34,7 +38,7 @@ private GithubProjectProperty createExampleProperty() { } @Test - public void shouldCreateGithubLinkActionForJobWithGithubProjectProperty() throws IOException { + void shouldCreateGithubLinkActionForJobWithGithubProjectProperty() throws IOException { final WorkflowJob job = createExampleJob(); final GithubProjectProperty property = createExampleProperty(); job.addProperty(property); @@ -48,7 +52,7 @@ public void shouldCreateGithubLinkActionForJobWithGithubProjectProperty() throws } @Test - public void shouldNotCreateGithubLinkActionForJobWithoutGithubProjectProperty() throws IOException { + void shouldNotCreateGithubLinkActionForJobWithoutGithubProjectProperty() throws IOException { final WorkflowJob job = createExampleJob(); final Collection actions = factory.createFor(job); diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java index aba3bb86e..3cf8f517e 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubLinkAnnotatorTest.java @@ -1,33 +1,34 @@ package com.coravy.hudson.plugins.github; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; import hudson.MarkupText; import hudson.plugins.git.GitChangeSet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.jvnet.hudson.test.Issue; + import java.util.ArrayList; import java.util.Random; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; + import static java.lang.String.format; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; -@RunWith(DataProviderRunner.class) -public class GithubLinkAnnotatorTest { +class GithubLinkAnnotatorTest { - private final static String GITHUB_URL = "http://github.com/juretta/iphone-project-tools"; - private final static String SHA1 = "badbeef136cd854f4dd6fa40bf94c0c657681dd5"; - private final static Random RANDOM = new Random(); + private static final String GITHUB_URL = "http://github.com/juretta/iphone-project-tools"; + private static final String SHA1 = "badbeef136cd854f4dd6fa40bf94c0c657681dd5"; + private static final Random RANDOM = new Random(); private final String expectedChangeSetAnnotation = " (" + "" + "commit: " + SHA1.substring(0, 7) + ")"; private static GitChangeSet changeSet; - @Before - public void createChangeSet() throws Exception { + @BeforeEach + void createChangeSet() throws Exception { ArrayList lines = new ArrayList(); lines.add("commit " + SHA1); lines.add("tree 66236cf9a1ac0c589172b450ed01f019a5697c49"); @@ -43,7 +44,7 @@ public void createChangeSet() throws Exception { private static Object[] genActualAndExpected(String keyword) { int issueNumber = RANDOM.nextInt(1000000); final String innerText = keyword + " #" + issueNumber; - final String startHREF = ""; + final String startHREF = ""; final String endHREF = ""; final String annotatedText = startHREF + innerText + endHREF; return new Object[]{ @@ -54,8 +55,7 @@ private static Object[] genActualAndExpected(String keyword) { }; } - @DataProvider - public static Object[][] annotations() { + static Object[][] annotations() { return new Object[][]{ genActualAndExpected("Closes"), genActualAndExpected("Close"), @@ -64,22 +64,40 @@ public static Object[][] annotations() { }; } - @Test - @UseDataProvider("annotations") - public void inputIsExpected(String input, String expected) throws Exception { + @ParameterizedTest + @MethodSource("annotations") + void inputIsExpected(String input, String expected) throws Exception { assertThat(format("For input '%s'", input), annotate(input, null), is(expected)); } - @Test - @UseDataProvider("annotations") - public void inputIsExpectedWithChangeSet(String input, String expected) throws Exception { + @ParameterizedTest + @MethodSource("annotations") + void inputIsExpectedWithChangeSet(String input, String expected) throws Exception { assertThat(format("For changeset input '%s'", input), annotate(input, changeSet), is(expected + expectedChangeSetAnnotation)); } + //Test to verify that fake url starting with sentences like javascript are not validated + @Test + @Issue("SECURITY-3246") + void urlValidationTest() { + GithubLinkAnnotator annotator = new GithubLinkAnnotator(); + assertThrows(IllegalArgumentException.class, () -> + annotator.annotate(new GithubUrl("javascript:alert(1); //"), null, null)); + } + + //Test to verify that fake url are not validated + @Test + @Issue("SECURITY-3246") + void urlHtmlAttributeValidationTest() { + GithubLinkAnnotator annotator = new GithubLinkAnnotator(); + assertThrows(IllegalArgumentException.class, () -> + annotator.annotate(new GithubUrl("a' onclick=alert(777) foo='bar/\n"), null, null)); + } + private String annotate(final String originalText, GitChangeSet changeSet) { MarkupText markupText = new MarkupText(originalText); diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java index 545e5aff5..99389402f 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubProjectPropertyTest.java @@ -2,20 +2,29 @@ import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.structs.DescribableHelper; -import org.junit.Ignore; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -@Ignore("It failed to instantiate class org.jenkinsci.plugins.workflow.flow.FlowDefinition - dunno how to fix it") -public class GithubProjectPropertyTest { +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; - @Rule - public JenkinsRule j = new JenkinsRule(); +@WithJenkins +@Disabled("It failed to instantiate class org.jenkinsci.plugins.workflow.flow.FlowDefinition - dunno how to fix it") +class GithubProjectPropertyTest { + + private JenkinsRule j; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + j = rule; + } @Test - public void configRoundTrip() throws Exception { + void configRoundTrip() throws Exception { WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p"); j.configRoundtrip(p); assertNull(p.getProperty(GithubProjectProperty.class)); diff --git a/src/test/java/com/coravy/hudson/plugins/github/GithubUrlTest.java b/src/test/java/com/coravy/hudson/plugins/github/GithubUrlTest.java index 702dd9941..fae3d9427 100644 --- a/src/test/java/com/coravy/hudson/plugins/github/GithubUrlTest.java +++ b/src/test/java/com/coravy/hudson/plugins/github/GithubUrlTest.java @@ -1,23 +1,13 @@ package com.coravy.hudson.plugins.github; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class GithubUrlTest { - - @Before - public void setUp() throws Exception { - } - - @After - public void tearDown() throws Exception { - } +class GithubUrlTest { @Test - public final void testBaseUrlWithTree() { + void testBaseUrlWithTree() { GithubUrl url = new GithubUrl( "http://github.com/juretta/iphone-project-tools/tree/master"); assertEquals("http://github.com/juretta/iphone-project-tools/", url @@ -29,7 +19,7 @@ public final void testBaseUrlWithTree() { } @Test - public final void testBaseUrl() { + void testBaseUrl() { GithubUrl url = new GithubUrl( "http://github.com/juretta/iphone-project-tools"); assertEquals("http://github.com/juretta/iphone-project-tools/", url @@ -37,7 +27,7 @@ public final void testBaseUrl() { } @Test - public final void testCommitId() { + void testCommitId() { GithubUrl url = new GithubUrl( "http://github.com/juretta/hudson-github-plugin/tree/master"); assertEquals( diff --git a/src/test/java/org/jenkinsci/plugins/github/admin/GHRepoNameTest.java b/src/test/java/org/jenkinsci/plugins/github/admin/GHRepoNameTest.java index e95f695c2..9e1540d0c 100644 --- a/src/test/java/org/jenkinsci/plugins/github/admin/GHRepoNameTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/admin/GHRepoNameTest.java @@ -1,34 +1,34 @@ package org.jenkinsci.plugins.github.admin; import com.cloudbees.jenkins.GitHubRepositoryName; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.kohsuke.stapler.StaplerRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.stapler.StaplerRequest2; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class GHRepoNameTest { +@ExtendWith(MockitoExtension.class) +class GHRepoNameTest { public static final String REPO_NAME_PARAMETER = "repo"; private static final String REPO = "https://github.com/user/repo"; @Mock - private StaplerRequest req; + private StaplerRequest2 req; @Mock private GHRepoName anno; @Test - public void shouldExtractRepoNameFromForm() throws Exception { + void shouldExtractRepoNameFromForm() throws Exception { when(req.getParameter(REPO_NAME_PARAMETER)).thenReturn(REPO); GitHubRepositoryName repo = new GHRepoName.PayloadHandler().parse(req, anno, null, REPO_NAME_PARAMETER); @@ -36,7 +36,7 @@ public void shouldExtractRepoNameFromForm() throws Exception { } @Test - public void shouldReturnNullOnNoAnyParam() throws Exception { + void shouldReturnNullOnNoAnyParam() throws Exception { GitHubRepositoryName repo = new GHRepoName.PayloadHandler().parse(req, anno, null, REPO_NAME_PARAMETER); assertThat("should not parse repo", repo, nullValue()); diff --git a/src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitorTest.java b/src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitorTest.java new file mode 100644 index 000000000..695c607b8 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitorTest.java @@ -0,0 +1,134 @@ +package org.jenkinsci.plugins.github.admin; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.net.URL; + +import org.htmlunit.HttpMethod; + +import org.htmlunit.WebRequest; +import org.htmlunit.html.HtmlElementUtil; +import org.htmlunit.html.HtmlPage; +import org.jenkinsci.plugins.github.Messages; +import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.JenkinsRule.WebClient; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import org.mockito.Mockito; +import org.xml.sax.SAXException; + +import hudson.ExtensionList; + +@WithJenkins +class GitHubDuplicateEventsMonitorTest { + + private JenkinsRule j; + + private GitHubDuplicateEventsMonitor monitor; + private WebClient wc; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + j = rule; + monitor = ExtensionList.lookupSingleton(GitHubDuplicateEventsMonitor.class); + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + wc = j.createWebClient(); + wc.login("admin", "admin"); + } + + @Test + void testAdminMonitorDisplaysForDuplicateEvents() throws Exception { + try (var mockSubscriber = Mockito.mockStatic(GHEventsSubscriber.class)) { + var subscribers = j.jenkins.getExtensionList(GHEventsSubscriber.class); + /* Other type of subscribers are removed to avoid them invoking event processing. At this + time, when using the `push` event type, the `DefaultGHEventsSubscriber` gets invoked, and throws + an NPE during processing of the event. This is because the `GHEvent` object here is not fully initialized. + However, as this test is only concerned with the duplicate event detection, it doesn't seem to add value + in fixing for the NPE. Alternatively, we may choose to send an event which is not subscribed + by other subscribers (ex: `check_run`), but that would only work until someone adds a new subscriber for + that event type, at which point, a new event type would need to be chosen in here. + * */ + var nonDuplicateSubscribers = subscribers.stream() + .filter(e -> !(e instanceof GitHubDuplicateEventsMonitor.DuplicateEventsSubscriber)) + .toList(); + nonDuplicateSubscribers.forEach(subscribers::remove); + mockSubscriber.when(GHEventsSubscriber::all).thenReturn(subscribers); + + // to begin with, monitor doesn't show automatically + assertMonitorNotDisplayed(); + + // normal case: unique events don't cause admin monitor + sendGHEvents(wc, "event1"); + sendGHEvents(wc, "event2"); + assertMonitorNotDisplayed(); + + // duplicate events cause admin monitor + var event3 = "event3"; + sendGHEvents(wc, event3); + sendGHEvents(wc, event3); + assertMonitorDisplayed(event3); + + // send a new duplicate + var event4 = "event4"; + sendGHEvents(wc, event4); + sendGHEvents(wc, event4); + assertMonitorDisplayed(event4); + } + } + + private void sendGHEvents(WebClient wc, String eventGuid) throws IOException { + wc.addRequestHeader("Content-Type", "application/json"); + wc.addRequestHeader("X-GitHub-Delivery", eventGuid); + wc.addRequestHeader("X-Github-Event", "push"); + String url = j.getURL() + "/github-webhook/"; + var webRequest = new WebRequest(new URL(url), HttpMethod.POST); + webRequest.setRequestBody(getJsonPayload(eventGuid)); + assertThat(wc.getPage(webRequest).getWebResponse().getStatusCode(), is(200)); + } + + private void assertMonitorNotDisplayed() throws IOException, SAXException { + String manageUrl = j.getURL() + "/manage"; + assertThat( + wc.getPage(manageUrl).getWebResponse().getContentAsString(), + not(containsString(Messages.duplicate_events_administrative_monitor_blurb( + GitHubDuplicateEventsMonitor.LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID, + monitor.getLastDuplicateUrl() + )))); + assertEquals(GitHubDuplicateEventsMonitor.getLastDuplicateNoEventPayload().toString(), + getLastDuplicatePageContentByLink()); + } + + private void assertMonitorDisplayed(String eventGuid) throws IOException, SAXException { + String manageUrl = j.getURL() + "/manage"; + assertThat( + wc.getPage(manageUrl).getWebResponse().getContentAsString(), + containsString(Messages.duplicate_events_administrative_monitor_blurb( + GitHubDuplicateEventsMonitor.LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID, + monitor.getLastDuplicateUrl()))); + assertEquals(getJsonPayload(eventGuid), getLastDuplicatePageContentByAnchor()); + } + + private String getLastDuplicatePageContentByAnchor() throws IOException, SAXException { + HtmlPage page = wc.goTo("./manage"); + var lastDuplicateAnchor = page.getAnchors().stream().filter( + a -> a.getId().equals(GitHubDuplicateEventsMonitor.LAST_DUPLICATE_CLICK_HERE_ANCHOR_ID) + ).findFirst(); + var lastDuplicatePage = HtmlElementUtil.click(lastDuplicateAnchor.get()); + return lastDuplicatePage.getWebResponse().getContentAsString(); + } + + private String getLastDuplicatePageContentByLink() throws IOException, SAXException { + return wc.goTo(monitor.getLastDuplicateUrl(), "application/json").getWebResponse().getContentAsString(); + } + + private String getJsonPayload(String eventGuid) { + return "{\"payload\":\"" + eventGuid + "\"}"; + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitorUnitTest.java b/src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitorUnitTest.java new file mode 100644 index 000000000..ef19cd66c --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitorUnitTest.java @@ -0,0 +1,115 @@ +package org.jenkinsci.plugins.github.admin; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import java.time.Duration; +import java.time.Instant; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import com.github.benmanes.caffeine.cache.Ticker; +import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.For; +import org.kohsuke.github.GHEvent; + +@For(GitHubDuplicateEventsMonitor.DuplicateEventsSubscriber.class) +class GitHubDuplicateEventsMonitorUnitTest { + + @Test + void onEventShouldTrackEventAndKeepTrackOfLastDuplicate() { + var subscriber = new GitHubDuplicateEventsMonitor.DuplicateEventsSubscriber(); + + var now = Instant.parse("2025-02-05T03:00:00Z"); + var after1Sec = Instant.parse("2025-02-05T03:00:01Z"); + var after2Sec = Instant.parse("2025-02-05T03:00:02Z"); + FakeTicker fakeTicker = new FakeTicker(now); + subscriber.setTicker(fakeTicker); + + assertThat("lastDuplicate is null at first", subscriber.getLastDuplicate(), is(nullValue())); + assertThat("should not throw NPE", subscriber.isDuplicateEventSeen(), is(false)); + // send a null event + subscriber.onEvent(new GHSubscriberEvent(null, "origin", GHEvent.PUSH, "payload")); + assertThat("null event is not tracked", subscriber.getPresentEventKeys().size(), is(0)); + assertThat("lastDuplicate is still null", subscriber.getLastDuplicate(), is(nullValue())); + + // at present + subscriber.onEvent(new GHSubscriberEvent("1", "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1"))); + assertThat(subscriber.getLastDuplicate(), is(nullValue())); + assertThat(subscriber.isDuplicateEventSeen(), is(false)); + subscriber.onEvent(new GHSubscriberEvent("2", "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getLastDuplicate(), is(nullValue())); + assertThat(subscriber.isDuplicateEventSeen(), is(false)); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1", "2"))); + subscriber.onEvent(new GHSubscriberEvent(null, "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getLastDuplicate(), is(nullValue())); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1", "2"))); + assertThat(subscriber.isDuplicateEventSeen(), is(false)); + + // after a second + fakeTicker.advance(Duration.ofSeconds(1)); + subscriber.onEvent(new GHSubscriberEvent("1", "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getLastDuplicate().eventGuid(), is("1")); + assertThat(subscriber.getLastDuplicate().lastUpdated(), is(after1Sec)); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1", "2"))); + assertThat(subscriber.isDuplicateEventSeen(), is(true)); + + // second occurrence for another event after 2 seconds + fakeTicker.advance(Duration.ofSeconds(1)); + subscriber.onEvent(new GHSubscriberEvent("2", "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getLastDuplicate().eventGuid(), is("2")); + assertThat(subscriber.getLastDuplicate().lastUpdated(), is(after2Sec)); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1", "2"))); + assertThat(subscriber.isDuplicateEventSeen(), is(true)); + + // 24 hours has passed; note we already added 2 seconds/ so effectively 24h 2sec now. + fakeTicker.advance(Duration.ofHours(24)); + assertThat(subscriber.isDuplicateEventSeen(), is(false)); + } + + @Test + void checkOldEntriesAreExpiredAfter10Minutes() { + var subscriber = new GitHubDuplicateEventsMonitor.DuplicateEventsSubscriber(); + + var now = Instant.parse("2025-02-05T03:00:00Z"); + FakeTicker fakeTicker = new FakeTicker(now); + subscriber.setTicker(fakeTicker); + + // at present + subscriber.onEvent(new GHSubscriberEvent("1", "origin", GHEvent.PUSH, "payload")); + subscriber.onEvent(new GHSubscriberEvent("2", "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1", "2"))); + + // after 2 minutes + fakeTicker.advance(Duration.ofMinutes(2)); + subscriber.onEvent(new GHSubscriberEvent("3", "origin", GHEvent.PUSH, "payload")); + subscriber.onEvent(new GHSubscriberEvent("4", "origin", GHEvent.PUSH, "payload")); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("1", "2", "3", "4"))); + assertThat(subscriber.getPresentEventKeys().size(), is(4)); + + // 10 minutes 1 second later + fakeTicker.advance(Duration.ofMinutes(8).plusSeconds(1)); + assertThat(subscriber.getPresentEventKeys(), is(Set.of("3", "4"))); + assertThat(subscriber.getPresentEventKeys().size(), is(2)); + } + + private static class FakeTicker implements Ticker { + private final AtomicLong nanos = new AtomicLong(); + + FakeTicker(Instant now) { + nanos.set(now.toEpochMilli() * 1_000_000); + } + + @Override + public long read() { + return nanos.get(); + } + + public void advance(Duration duration) { + nanos.addAndGet(duration.toNanos()); + } + } +} diff --git a/src/test/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitorTest.java b/src/test/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitorTest.java index 8a4f3e875..6738ed09b 100644 --- a/src/test/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitorTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/admin/GitHubHookRegisterProblemMonitorTest.java @@ -5,33 +5,34 @@ import hudson.model.FreeStyleProject; import hudson.model.Item; import hudson.plugins.git.GitSCM; -import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; +import jakarta.inject.Inject; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.jenkinsci.plugins.github.extension.GHEventsSubscriber; +import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; import org.jenkinsci.plugins.github.webhook.WebhookManager; import org.jenkinsci.plugins.github.webhook.WebhookManagerTest; import org.jenkinsci.plugins.github.webhook.subscriber.PingGHEventSubscriber; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.jvnet.hudson.test.recipes.LocalData; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -import javax.inject.Inject; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import static com.cloudbees.jenkins.GitHubRepositoryName.create; import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -39,15 +40,15 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ @Issue("JENKINS-24690") -@RunWith(MockitoJUnitRunner.class) -public class GitHubHookRegisterProblemMonitorTest { +@WithJenkins +@ExtendWith(MockitoExtension.class) +class GitHubHookRegisterProblemMonitorTest { private static final GitHubRepositoryName REPO = new GitHubRepositoryName("host", "user", "repo"); private static final String REPO_GIT_URI = "host/user/repo.git"; private static final GitSCM REPO_GIT_SCM = new GitSCM("git://"+REPO_GIT_URI); @@ -63,12 +64,11 @@ public class GitHubHookRegisterProblemMonitorTest { @Inject private PingGHEventSubscriber pingSubscr; - @Rule - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private GitHub github; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private GHRepository ghRepository; class GitHubServerConfigForTest extends GitHubServerConfig { @@ -78,8 +78,9 @@ public GitHubServerConfigForTest(String credentialsId) { } } - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jRule = rule; jRule.getInstance().getInjector().injectMembers(this); GitHubServerConfig config = new GitHubServerConfigForTest(""); config.setApiUrl("http://" + REPO_GIT_URI); @@ -89,13 +90,13 @@ public void setUp() throws Exception { } @Test - public void shouldRegisterProblem() throws Exception { + void shouldRegisterProblem() throws Exception { monitor.registerProblem(REPO, new IOException()); assertThat("should register problem", monitor.isProblemWith(REPO), is(true)); } @Test - public void shouldResolveProblem() throws Exception { + void shouldResolveProblem() throws Exception { monitor.registerProblem(REPO, new IOException()); monitor.resolveProblem(REPO); @@ -103,19 +104,19 @@ public void shouldResolveProblem() throws Exception { } @Test - public void shouldNotAddNullRepo() throws Exception { + void shouldNotAddNullRepo() throws Exception { monitor.registerProblem(null, new IOException()); assertThat("should be no problems", monitor.getProblems().keySet(), empty()); } @Test - public void shouldNotAddNullExc() throws Exception { + void shouldNotAddNullExc() throws Exception { monitor.registerProblem(REPO, null); assertThat("should be no problems", monitor.getProblems().keySet(), empty()); } @Test - public void shouldDoNothingOnNullResolve() throws Exception { + void shouldDoNothingOnNullResolve() throws Exception { monitor.registerProblem(REPO, new IOException()); monitor.resolveProblem(null); @@ -123,18 +124,18 @@ public void shouldDoNothingOnNullResolve() throws Exception { } @Test - public void shouldBeDeactivatedByDefault() throws Exception { + void shouldBeDeactivatedByDefault() throws Exception { assertThat("should be deactivated", monitor.isActivated(), is(false)); } @Test - public void shouldBeActivatedOnProblems() throws Exception { + void shouldBeActivatedOnProblems() throws Exception { monitor.registerProblem(REPO, new IOException()); assertThat("active on problems", monitor.isActivated(), is(true)); } @Test - public void shouldResolveOnIgnoring() throws Exception { + void shouldResolveOnIgnoring() throws Exception { monitor.registerProblem(REPO, new IOException()); monitor.doIgnore(REPO); @@ -142,7 +143,7 @@ public void shouldResolveOnIgnoring() throws Exception { } @Test - public void shouldNotRegisterNewOnIgnoring() throws Exception { + void shouldNotRegisterNewOnIgnoring() throws Exception { monitor.doIgnore(REPO); monitor.registerProblem(REPO, new IOException()); @@ -150,7 +151,7 @@ public void shouldNotRegisterNewOnIgnoring() throws Exception { } @Test - public void shouldRemoveFromIgnoredOnDisignore() throws Exception { + void shouldRemoveFromIgnoredOnDisignore() throws Exception { monitor.doIgnore(REPO); monitor.doDisignore(REPO); @@ -158,7 +159,7 @@ public void shouldRemoveFromIgnoredOnDisignore() throws Exception { } @Test - public void shouldNotAddRepoTwiceToIgnore() throws Exception { + void shouldNotAddRepoTwiceToIgnore() throws Exception { monitor.doIgnore(REPO); monitor.doIgnore(REPO); @@ -167,12 +168,12 @@ public void shouldNotAddRepoTwiceToIgnore() throws Exception { @Test @LocalData - public void shouldLoadIgnoredList() throws Exception { + void shouldLoadIgnoredList() throws Exception { assertThat("loaded", monitor.getIgnored(), hasItem(equalTo(REPO))); } @Test - public void shouldReportAboutHookProblemOnRegister() throws IOException { + void shouldReportAboutHookProblemOnRegister() throws IOException { FreeStyleProject job = jRule.createFreeStyleProject(); job.addTrigger(new GitHubPushTrigger()); job.setScm(REPO_GIT_SCM); @@ -186,7 +187,7 @@ public void shouldReportAboutHookProblemOnRegister() throws IOException { } @Test - public void shouldNotReportAboutHookProblemOnRegister() throws IOException { + void shouldNotReportAboutHookProblemOnRegister() throws IOException { FreeStyleProject job = jRule.createFreeStyleProject(); job.addTrigger(new GitHubPushTrigger()); job.setScm(REPO_GIT_SCM); @@ -198,7 +199,7 @@ public void shouldNotReportAboutHookProblemOnRegister() throws IOException { } @Test - public void shouldReportAboutHookProblemOnUnregister() throws IOException { + void shouldReportAboutHookProblemOnUnregister() throws IOException { when(github.getRepository("user/repo")) .thenThrow(new RuntimeException("shouldReportAboutHookProblemOnUnregister")); WebhookManager.forHookUrl(WebhookManagerTest.HOOK_ENDPOINT) @@ -208,7 +209,7 @@ public void shouldReportAboutHookProblemOnUnregister() throws IOException { } @Test - public void shouldNotReportAboutHookAuthProblemOnUnregister() { + void shouldNotReportAboutHookAuthProblemOnUnregister() { WebhookManager.forHookUrl(WebhookManagerTest.HOOK_ENDPOINT) .unregisterFor(REPO, Collections.emptyList()); @@ -216,7 +217,7 @@ public void shouldNotReportAboutHookAuthProblemOnUnregister() { } @Test - public void shouldResolveOnPingHook() { + void shouldResolveOnPingHook() { monitor.registerProblem(REPO_FROM_PING_PAYLOAD, new IOException()); GHEventsSubscriber.processEvent(new GHSubscriberEvent("shouldResolveOnPingHook", GHEvent.PING, classpath("payloads/ping.json"))).apply(pingSubscr); @@ -225,26 +226,26 @@ public void shouldResolveOnPingHook() { } @Test - public void shouldShowManagementLinkIfNonEmptyProblems() throws Exception { + void shouldShowManagementLinkIfNonEmptyProblems() throws Exception { monitor.registerProblem(REPO, new IOException()); assertThat("link on problems", link.getIconFileName(), notNullValue()); } @Test - public void shouldShowManagementLinkIfNonEmptyIgnores() throws Exception { + void shouldShowManagementLinkIfNonEmptyIgnores() throws Exception { monitor.doIgnore(REPO); assertThat("link on ignores", link.getIconFileName(), notNullValue()); } @Test - public void shouldShowManagementLinkIfBoth() throws Exception { + void shouldShowManagementLinkIfBoth() throws Exception { monitor.registerProblem(REPO_FROM_PING_PAYLOAD, new IOException()); monitor.doIgnore(REPO); assertThat("link on ignores", link.getIconFileName(), notNullValue()); } @Test - public void shouldNotShowManagementLinkIfNoAny() throws Exception { + void shouldNotShowManagementLinkIfNoAny() throws Exception { assertThat("link on no any", link.getIconFileName(), nullValue()); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/admin/ValidateRepoNameTest.java b/src/test/java/org/jenkinsci/plugins/github/admin/ValidateRepoNameTest.java index 4cb120809..4f79e5229 100644 --- a/src/test/java/org/jenkinsci/plugins/github/admin/ValidateRepoNameTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/admin/ValidateRepoNameTest.java @@ -1,23 +1,23 @@ package org.jenkinsci.plugins.github.admin; import com.cloudbees.jenkins.GitHubRepositoryName; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.stapler.Function; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.StaplerRequest2; +import org.kohsuke.stapler.StaplerResponse2; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.lang.reflect.InvocationTargetException; +import static org.junit.jupiter.api.Assertions.assertThrows; + /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class ValidateRepoNameTest { +@ExtendWith(MockitoExtension.class) +class ValidateRepoNameTest { public static final Object ANY_INSTANCE = null; public static final GitHubRepositoryName VALID_REPO = new GitHubRepositoryName("", "", ""); @@ -25,26 +25,23 @@ public class ValidateRepoNameTest { private Function target; @Mock - private StaplerRequest req; + private StaplerRequest2 req; @Mock - private StaplerResponse resp; - - @Rule - public ExpectedException exc = ExpectedException.none(); + private StaplerResponse2 resp; @Test - public void shouldThrowInvocationExcOnNullsInArgs() throws Exception { - ValidateRepoName.Processor processor = new ValidateRepoName.Processor(); - processor.setTarget(target); - - exc.expect(InvocationTargetException.class); + void shouldThrowInvocationExcOnNullsInArgs() { + assertThrows(InvocationTargetException.class, () -> { + ValidateRepoName.Processor processor = new ValidateRepoName.Processor(); + processor.setTarget(target); - processor.invoke(req, resp, ANY_INSTANCE, new Object[]{null}); + processor.invoke(req, resp, ANY_INSTANCE, new Object[]{null}); + }); } @Test - public void shouldNotThrowInvocationExcNameInArgs() throws Exception { + void shouldNotThrowInvocationExcNameInArgs() throws Exception { ValidateRepoName.Processor processor = new ValidateRepoName.Processor(); processor.setTarget(target); diff --git a/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java b/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java index 1fc88683d..737ce8624 100644 --- a/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/common/CombineErrorHandlerTest.java @@ -5,21 +5,19 @@ import hudson.model.TaskListener; import org.jenkinsci.plugins.github.status.err.ChangingBuildStatusErrorHandler; import org.jenkinsci.plugins.github.status.err.ShallowAnyErrorHandler; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -import javax.annotation.Nonnull; import java.util.Collections; import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.jenkinsci.plugins.github.common.CombineErrorHandler.errorHandling; -import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -27,8 +25,8 @@ /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class CombineErrorHandlerTest { +@ExtendWith(MockitoExtension.class) +class CombineErrorHandlerTest { @Mock(answer = Answers.RETURNS_MOCKS) private Run run; @@ -36,25 +34,22 @@ public class CombineErrorHandlerTest { @Mock private TaskListener listener; - @Rule - public ExpectedException exc = ExpectedException.none(); - @Test - public void shouldRethrowExceptionIfNoMatch() throws Exception { - exc.expect(CombineErrorHandler.ErrorHandlingException.class); + void shouldRethrowExceptionIfNoMatch() { + assertThrows(CombineErrorHandler.ErrorHandlingException.class, () -> - errorHandling().handle(new RuntimeException(), run, listener); + errorHandling().handle(new RuntimeException(), run, listener)); } @Test - public void shouldRethrowExceptionIfNullHandlersList() throws Exception { - exc.expect(CombineErrorHandler.ErrorHandlingException.class); + void shouldRethrowExceptionIfNullHandlersList() { + assertThrows(CombineErrorHandler.ErrorHandlingException.class, () -> - errorHandling().withHandlers(null).handle(new RuntimeException(), run, listener); + errorHandling().withHandlers(null).handle(new RuntimeException(), run, listener)); } @Test - public void shouldHandleExceptionsWithHandler() throws Exception { + void shouldHandleExceptionsWithHandler() throws Exception { boolean handled = errorHandling() .withHandlers(Collections.singletonList(new ShallowAnyErrorHandler())) .handle(new RuntimeException(), run, listener); @@ -63,23 +58,20 @@ public void shouldHandleExceptionsWithHandler() throws Exception { } @Test - public void shouldRethrowExceptionIfExceptionInside() throws Exception { - exc.expect(CombineErrorHandler.ErrorHandlingException.class); - - errorHandling() - .withHandlers(Collections.singletonList( - new ErrorHandler() { - @Override - public boolean handle(Exception e, @Nonnull Run run, @Nonnull TaskListener listener) { + void shouldRethrowExceptionIfExceptionInside() { + assertThrows(CombineErrorHandler.ErrorHandlingException.class, () -> + + errorHandling() + .withHandlers(Collections.singletonList( + (e, run, listener) -> { throw new RuntimeException("wow"); } - } - )) - .handle(new RuntimeException(), run, listener); + )) + .handle(new RuntimeException(), run, listener)); } @Test - public void shouldHandleExceptionWithFirstMatchAndSetStatus() throws Exception { + void shouldHandleExceptionWithFirstMatchAndSetStatus() throws Exception { boolean handled = errorHandling() .withHandlers(asList( new ChangingBuildStatusErrorHandler(Result.FAILURE.toString()), diff --git a/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java b/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java index bac327f22..cf96bdc0b 100644 --- a/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/common/ExpandableMessageTest.java @@ -9,10 +9,11 @@ import hudson.model.ParametersDefinitionProperty; import hudson.model.StringParameterDefinition; import hudson.model.StringParameterValue; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestBuilder; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -24,7 +25,8 @@ /** * @author lanwen (Merkushev Kirill) */ -public class ExpandableMessageTest { +@WithJenkins +class ExpandableMessageTest { public static final String ENV_VAR_JOB_NAME = "JOB_NAME"; public static final String CUSTOM_BUILD_PARAM = "FOO"; @@ -32,11 +34,15 @@ public class ExpandableMessageTest { public static final String MSG_FORMAT = "%s - %s - %s"; public static final String DEFAULT_TOKEN_TEMPLATE = "${ENV, var=\"%s\"}"; - @Rule - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jRule = rule; + } @Test - public void shouldExpandEnvAndBuildVars() throws Exception { + void shouldExpandEnvAndBuildVars() throws Exception { MessageExpander expander = new MessageExpander(new ExpandableMessage( format(MSG_FORMAT, asVar(ENV_VAR_JOB_NAME), diff --git a/src/test/java/org/jenkinsci/plugins/github/config/ConfigAsCodeTest.java b/src/test/java/org/jenkinsci/plugins/github/config/ConfigAsCodeTest.java index 984e6e848..053605235 100755 --- a/src/test/java/org/jenkinsci/plugins/github/config/ConfigAsCodeTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/ConfigAsCodeTest.java @@ -5,26 +5,37 @@ import io.jenkins.plugins.casc.ConfiguratorRegistry; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode; import io.jenkins.plugins.casc.model.CNode; import io.jenkins.plugins.casc.model.Mapping; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.*; - -public class ConfigAsCodeTest { - - @Rule - public JenkinsConfiguredWithCodeRule r = new JenkinsConfiguredWithCodeRule(); +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withApiUrl; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withApiUrlS; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withClientCacheSize; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withClientCacheSizeS; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withCredsId; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withCredsIdS; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withIsManageHooks; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withIsManageHooksS; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withName; +import static org.jenkinsci.plugins.github.test.GitHubServerConfigMatcher.withNameS; + +@WithJenkinsConfiguredWithCode +class ConfigAsCodeTest { @SuppressWarnings("deprecation") @Test @ConfiguredWithCode("configuration-as-code.yml") - public void shouldSupportConfigurationAsCode() throws Exception { + void shouldSupportConfigurationAsCode(JenkinsConfiguredWithCodeRule r) throws Exception { GitHubPluginConfig gitHubPluginConfig = GitHubPluginConfig.all().get(GitHubPluginConfig.class); @@ -62,7 +73,7 @@ public void shouldSupportConfigurationAsCode() throws Exception { @Test @ConfiguredWithCode("configuration-as-code.yml") - public void exportConfiguration() throws Exception { + void exportConfiguration(JenkinsConfiguredWithCodeRule r) throws Exception { GitHubPluginConfig globalConfiguration = GitHubPluginConfig.all().get(GitHubPluginConfig.class); ConfiguratorRegistry registry = ConfiguratorRegistry.get(); diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java index 6016ca78e..08327a5ba 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubPluginConfigTest.java @@ -3,18 +3,19 @@ import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.SystemCredentialsProvider; import com.cloudbees.plugins.credentials.domains.Domain; -import com.gargoylesoftware.htmlunit.HttpMethod; -import com.gargoylesoftware.htmlunit.Page; -import com.gargoylesoftware.htmlunit.WebRequest; import hudson.security.GlobalMatrixAuthorizationStrategy; import hudson.util.Secret; import jenkins.model.Jenkins; +import org.htmlunit.HttpMethod; +import org.htmlunit.Page; +import org.htmlunit.WebRequest; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import java.net.URL; import java.util.Arrays; @@ -26,29 +27,34 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author lanwen (Merkushev Kirill) */ -public class GitHubPluginConfigTest { +@WithJenkins +class GitHubPluginConfigTest { - @Rule - public JenkinsRule j = new JenkinsRule(); + private JenkinsRule j; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + j = rule; + } @Test - public void shouldNotManageHooksOnEmptyCreds() throws Exception { + void shouldNotManageHooksOnEmptyCreds() throws Exception { assertThat(GitHubPlugin.configuration().isManageHooks(), is(false)); } @Test - public void shouldManageHooksOnManagedConfig() throws Exception { + void shouldManageHooksOnManagedConfig() throws Exception { GitHubPlugin.configuration().getConfigs().add(new GitHubServerConfig("")); assertThat(GitHubPlugin.configuration().isManageHooks(), is(true)); } @Test - public void shouldNotManageHooksOnNotManagedConfig() throws Exception { + void shouldNotManageHooksOnNotManagedConfig() throws Exception { GitHubServerConfig conf = new GitHubServerConfig(""); conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); @@ -57,23 +63,23 @@ public void shouldNotManageHooksOnNotManagedConfig() throws Exception { @Test @Issue("SECURITY-799") - public void shouldNotAllowSSRFUsingHookUrl() throws Exception { + void shouldNotAllowSSRFUsingHookUrl() throws Exception { final String targetUrl = "www.google.com"; final URL urlForSSRF = new URL(j.getURL() + "descriptorByName/github-plugin-configuration/checkHookUrl?value=" + targetUrl); - + j.jenkins.setCrumbIssuer(null); j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); - + GlobalMatrixAuthorizationStrategy strategy = new GlobalMatrixAuthorizationStrategy(); strategy.add(Jenkins.ADMINISTER, "admin"); strategy.add(Jenkins.READ, "user"); j.jenkins.setAuthorizationStrategy(strategy); - + { // as read-only user JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.login("user"); - + Page page = wc.getPage(new WebRequest(urlForSSRF, HttpMethod.POST)); assertThat(page.getWebResponse().getStatusCode(), equalTo(403)); } @@ -81,7 +87,7 @@ public void shouldNotAllowSSRFUsingHookUrl() throws Exception { JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.login("admin"); - + Page page = wc.getPage(new WebRequest(urlForSSRF, HttpMethod.POST)); assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); } @@ -89,7 +95,7 @@ public void shouldNotAllowSSRFUsingHookUrl() throws Exception { JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.login("admin"); - + Page page = wc.getPage(new WebRequest(urlForSSRF, HttpMethod.GET)); assertThat(page.getWebResponse().getStatusCode(), not(equalTo(200))); } @@ -97,7 +103,7 @@ public void shouldNotAllowSSRFUsingHookUrl() throws Exception { @Test @Issue("JENKINS-62097") - public void configRoundtrip() throws Exception { + void configRoundtrip() throws Exception { assertHookSecrets(""); j.configRoundtrip(); assertHookSecrets(""); @@ -109,6 +115,7 @@ public void configRoundtrip() throws Exception { j.configRoundtrip(); assertHookSecrets("#1; #2"); } + private void assertHookSecrets(String expected) { assertEquals(expected, GitHubPlugin.configuration().getHookSecretConfigs().stream().map(HookSecretConfig::getHookSecret).filter(Objects::nonNull).map(Secret::getPlainText).collect(Collectors.joining("; "))); } diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigIntegrationTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigIntegrationTest.java index 6dd5a399b..ee21be574 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigIntegrationTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigIntegrationTest.java @@ -5,32 +5,32 @@ import com.cloudbees.plugins.credentials.CredentialsScope; import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.domains.Domain; -import com.gargoylesoftware.htmlunit.HttpMethod; -import com.gargoylesoftware.htmlunit.Page; -import com.gargoylesoftware.htmlunit.WebRequest; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import net.sf.json.JSONObject; import hudson.security.GlobalMatrixAuthorizationStrategy; import hudson.util.Secret; import jenkins.model.Jenkins; -import net.sf.json.JSONObject; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletContextHandler.Context; -import org.eclipse.jetty.servlet.ServletHolder; import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.htmlunit.HttpMethod; +import org.htmlunit.Page; +import org.htmlunit.WebRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.For; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import static org.hamcrest.MatcherAssert.assertThat; @@ -41,94 +41,79 @@ /** * Integration counterpart of GitHubServerConfigTest */ +@WithJenkins @For(GitHubServerConfig.class) -public class GitHubServerConfigIntegrationTest { - - @Rule - public JenkinsRule j = new JenkinsRule(); - - private Server server; +class GitHubServerConfigIntegrationTest { + + private JenkinsRule j; + + private HttpServer server; private AttackerServlet attackerServlet; private String attackerUrl; - - @Before - public void setupServer() throws Exception { + + @BeforeEach + void setupServer(JenkinsRule rule) throws Exception { + j = rule; setupAttackerServer(); } - - @After - public void stopServer() { - try { - server.stop(); - } catch (Exception e) { - e.printStackTrace(); - } + + + @AfterEach + void stopServer() { + server.stop(1); } - + private void setupAttackerServer() throws Exception { - this.server = new Server(); - ServerConnector serverConnector = new ServerConnector(this.server); - server.addConnector(serverConnector); - - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); - context.setContextPath("/*"); - + this.server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); this.attackerServlet = new AttackerServlet(); - ServletHolder servletHolder = new ServletHolder(attackerServlet); - context.addServlet(servletHolder, "/*"); - - server.setHandler(context); - - server.start(); - - String host = serverConnector.getHost(); - if (host == null) { - host = "localhost"; - } - - this.attackerUrl = "http://" + host + ":" + serverConnector.getLocalPort(); + this.server.createContext("/user", this.attackerServlet); + this.server.start(); + InetSocketAddress addr = this.server.getAddress(); + this.attackerUrl = String.format("http://%s:%d", addr.getHostString(), addr.getPort()); } - + @Test @Issue("SECURITY-804") - public void shouldNotAllow_CredentialsLeakage_usingVerifyCredentials() throws Exception { + void shouldNotAllow_CredentialsLeakage_usingVerifyCredentials() throws Exception { final String credentialId = "cred_id"; final String secret = "my-secret-access-token"; - + setupCredentials(credentialId, secret); - + final URL url = new URL( j.getURL() + "descriptorByName/org.jenkinsci.plugins.github.config.GitHubServerConfig/verifyCredentials?" + "apiUrl=" + attackerUrl + "&credentialsId=" + credentialId ); - + j.jenkins.setCrumbIssuer(null); j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); GlobalMatrixAuthorizationStrategy strategy = new GlobalMatrixAuthorizationStrategy(); - strategy.add(Jenkins.ADMINISTER, "admin"); + Jenkins.MANAGE.setEnabled(true); + strategy.add(Jenkins.MANAGE, "admin"); + strategy.add(Jenkins.READ, "admin"); strategy.add(Jenkins.READ, "user"); j.jenkins.setAuthorizationStrategy(strategy); - + { // as read-only user JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.login("user"); - + Page page = wc.getPage(new WebRequest(url, HttpMethod.POST)); assertThat(page.getWebResponse().getStatusCode(), equalTo(403)); - + assertThat(attackerServlet.secretCreds, isEmptyOrNullString()); } - { // only admin can verify the credentials + { // only admin (with Manage permission) can verify the credentials JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.login("admin"); - + Page page = wc.getPage(new WebRequest(url, HttpMethod.POST)); assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); - + assertThat(attackerServlet.secretCreds, not(isEmptyOrNullString())); attackerServlet.secretCreds = null; } @@ -136,14 +121,14 @@ public void shouldNotAllow_CredentialsLeakage_usingVerifyCredentials() throws Ex JenkinsRule.WebClient wc = j.createWebClient(); wc.getOptions().setThrowExceptionOnFailingStatusCode(false); wc.login("admin"); - + Page page = wc.getPage(new WebRequest(url, HttpMethod.GET)); assertThat(page.getWebResponse().getStatusCode(), not(equalTo(200))); - + assertThat(attackerServlet.secretCreds, isEmptyOrNullString()); } } - + private void setupCredentials(String credentialId, String secret) throws Exception { CredentialsStore store = CredentialsProvider.lookupStores(j.jenkins).iterator().next(); // currently not required to follow the UI restriction in terms of path constraint when hitting directly the URL @@ -151,26 +136,32 @@ private void setupCredentials(String credentialId, String secret) throws Excepti Credentials credentials = new StringCredentialsImpl(CredentialsScope.GLOBAL, credentialId, "", Secret.fromString(secret)); store.addCredentials(domain, credentials); } - - private static class AttackerServlet extends DefaultServlet { + + private static class AttackerServlet implements HttpHandler { + public String secretCreds; - + @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { - switch (request.getRequestURI()) { - case "/user": - this.onUser(request, response); - break; + public void handle(HttpExchange he) throws IOException { + if ("GET".equals(he.getRequestMethod())) { + this.onUser(he); + } else { + he.sendResponseHeaders(HttpURLConnection.HTTP_BAD_METHOD, -1); } } - private void onUser(HttpServletRequest request, HttpServletResponse response) throws IOException { - secretCreds = request.getHeader("Authorization"); - response.getWriter().write(JSONObject.fromObject( + private void onUser(HttpExchange he) throws IOException { + secretCreds = he.getRequestHeaders().getFirst("Authorization"); + String response = JSONObject.fromObject( new HashMap() {{ put("login", "alice"); }} - ).toString()); + ).toString(); + byte[] body = response.getBytes(StandardCharsets.UTF_8); + he.sendResponseHeaders(HttpURLConnection.HTTP_OK, body.length); + try (OutputStream os = he.getResponseBody()) { + os.write(body); + } } } } diff --git a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java index c1859bfaa..db6fb0939 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/GitHubServerConfigTest.java @@ -1,16 +1,16 @@ package org.jenkinsci.plugins.github.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.net.URI; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.allowedToManageHooks; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.isUrlCustom; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.withHost; -import static org.junit.Assert.assertThat; /** * @author lanwen (Merkushev Kirill) @@ -21,56 +21,56 @@ public class GitHubServerConfigTest { public static final String DEFAULT_GH_API_HOST = "api.github.com"; @Test - public void shouldMatchAllowedConfig() throws Exception { + void shouldMatchAllowedConfig() throws Exception { assertThat(allowedToManageHooks().apply(new GitHubServerConfig("")), is(true)); } @Test - public void shouldNotMatchNotAllowedConfig() throws Exception { + void shouldNotMatchNotAllowedConfig() throws Exception { GitHubServerConfig input = new GitHubServerConfig(""); input.setManageHooks(false); assertThat(allowedToManageHooks().apply(input), is(false)); } @Test - public void shouldMatchNonEqualToGHUrl() throws Exception { + void shouldMatchNonEqualToGHUrl() throws Exception { assertThat(isUrlCustom(CUSTOM_GH_SERVER), is(true)); } @Test - public void shouldNotMatchEmptyUrl() throws Exception { + void shouldNotMatchEmptyUrl() throws Exception { assertThat(isUrlCustom(""), is(false)); } @Test - public void shouldNotMatchNullUrl() throws Exception { + void shouldNotMatchNullUrl() throws Exception { assertThat(isUrlCustom(null), is(false)); } @Test - public void shouldNotMatchDefaultUrl() throws Exception { + void shouldNotMatchDefaultUrl() throws Exception { assertThat(isUrlCustom(GITHUB_URL), is(false)); } @Test - public void shouldMatchDefaultConfigWithGHDefaultHost() throws Exception { + void shouldMatchDefaultConfigWithGHDefaultHost() throws Exception { assertThat(withHost(DEFAULT_GH_API_HOST).apply(new GitHubServerConfig("")), is(true)); } @Test - public void shouldNotMatchNonDefaultConfigWithGHDefaultHost() throws Exception { + void shouldNotMatchNonDefaultConfigWithGHDefaultHost() throws Exception { GitHubServerConfig input = new GitHubServerConfig(""); input.setApiUrl(CUSTOM_GH_SERVER); assertThat(withHost(DEFAULT_GH_API_HOST).apply(input), is(false)); } @Test - public void shouldNotMatchDefaultConfigWithNonDefaultHost() throws Exception { + void shouldNotMatchDefaultConfigWithNonDefaultHost() throws Exception { assertThat(withHost(URI.create(CUSTOM_GH_SERVER).getHost()).apply(new GitHubServerConfig("")), is(false)); } @Test - public void shouldGuessNameIfNotProvided() throws Exception { + void shouldGuessNameIfNotProvided() throws Exception { GitHubServerConfig input = new GitHubServerConfig(""); input.setApiUrl(CUSTOM_GH_SERVER); assertThat(input.getName(), is(nullValue())); @@ -78,14 +78,14 @@ public void shouldGuessNameIfNotProvided() throws Exception { } @Test - public void shouldPickCorrectNamesForGitHub() throws Exception { + void shouldPickCorrectNamesForGitHub() throws Exception { GitHubServerConfig input = new GitHubServerConfig(""); assertThat(input.getName(), is(nullValue())); assertThat(input.getDisplayName(), is("GitHub (https://github.com)")); } @Test - public void shouldUseNameIfProvided() throws Exception { + void shouldUseNameIfProvided() throws Exception { GitHubServerConfig input = new GitHubServerConfig(""); input.setApiUrl(CUSTOM_GH_SERVER); input.setName("Test Example"); diff --git a/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigSHA256Test.java b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigSHA256Test.java new file mode 100644 index 000000000..eb17af282 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigSHA256Test.java @@ -0,0 +1,88 @@ +package org.jenkinsci.plugins.github.config; + +import org.jenkinsci.plugins.github.webhook.SignatureAlgorithm; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * Tests for SHA-256 configuration in {@link HookSecretConfig}. + * + * @since 1.45.0 + */ +class HookSecretConfigSHA256Test { + + @Test + void shouldDefaultToSHA256Algorithm() { + HookSecretConfig config = new HookSecretConfig("test-credentials"); + + assertThat("Should default to SHA-256 algorithm", + config.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA256)); + } + + @Test + void shouldAcceptExplicitSHA256Algorithm() { + HookSecretConfig config = new HookSecretConfig("test-credentials", "SHA256"); + + assertThat("Should use explicitly set SHA-256 algorithm", + config.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA256)); + } + + @Test + void shouldAcceptSHA1Algorithm() { + HookSecretConfig config = new HookSecretConfig("test-credentials", "SHA1"); + + assertThat("Should use explicitly set SHA-1 algorithm", + config.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA1)); + } + + @Test + void shouldDefaultToSHA256WhenNullAlgorithmProvided() { + HookSecretConfig config = new HookSecretConfig("test-credentials", null); + + assertThat("Should default to SHA-256 when null algorithm provided", + config.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA256)); + } + + @Test + void shouldDefaultToSHA256WhenInvalidAlgorithmProvided() { + HookSecretConfig config = new HookSecretConfig("test-credentials", "INVALID"); + + assertThat("Should default to SHA-256 when invalid algorithm provided", + config.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA256)); + } + + @Test + void shouldBeCaseInsensitive() { + HookSecretConfig config1 = new HookSecretConfig("test-credentials", "sha256"); + HookSecretConfig config2 = new HookSecretConfig("test-credentials", "Sha1"); + + assertThat("Should handle lowercase SHA-256", + config1.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA256)); + assertThat("Should handle mixed case SHA-1", + config2.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA1)); + } + + @Test + void shouldRespectSystemPropertyOverride() { + // Save original property + String originalProperty = System.getProperty("jenkins.github.webhook.signature.default"); + + try { + // Test SHA1 override + System.setProperty("jenkins.github.webhook.signature.default", "SHA1"); + HookSecretConfig config = new HookSecretConfig("test-credentials"); + + assertThat("Should use SHA-1 when system property is set", + config.getSignatureAlgorithm(), equalTo(SignatureAlgorithm.SHA1)); + } finally { + // Restore original property + if (originalProperty != null) { + System.setProperty("jenkins.github.webhook.signature.default", originalProperty); + } else { + System.clearProperty("jenkins.github.webhook.signature.default"); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java index d5d4bf708..98889a813 100644 --- a/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/config/HookSecretConfigTest.java @@ -1,51 +1,51 @@ package org.jenkinsci.plugins.github.config; import org.jenkinsci.plugins.github.GitHubPlugin; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecret; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Test for storing hook secrets. */ +@WithJenkins @SuppressWarnings("deprecation") -public class HookSecretConfigTest { +class HookSecretConfigTest { private static final String SECRET_INIT = "test"; - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); + private JenkinsRule jenkinsRule; private HookSecretConfig hookSecretConfig; - @Before - public void setup() { + @BeforeEach + void setup(JenkinsRule rule) { + jenkinsRule = rule; storeSecret(SECRET_INIT); } @Test - public void shouldStoreNewSecrets() { + void shouldStoreNewSecrets() { storeSecret(SECRET_INIT); hookSecretConfig = GitHubPlugin.configuration().getHookSecretConfig(); - assertNotNull("Secret is persistent", hookSecretConfig.getHookSecret()); - assertTrue("Secret correctly stored", SECRET_INIT.equals(hookSecretConfig.getHookSecret().getPlainText())); + assertNotNull(hookSecretConfig.getHookSecret(), "Secret is persistent"); + assertEquals(SECRET_INIT, hookSecretConfig.getHookSecret().getPlainText(), "Secret correctly stored"); } @Test - public void shouldOverwriteExistingSecrets() { + void shouldOverwriteExistingSecrets() { final String newSecret = "test2"; storeSecret(newSecret); hookSecretConfig = GitHubPlugin.configuration().getHookSecretConfig(); - assertNotNull("Secret is persistent", hookSecretConfig.getHookSecret()); - assertEquals("Secret correctly stored", newSecret, hookSecretConfig.getHookSecret().getPlainText()); + assertNotNull(hookSecretConfig.getHookSecret(), "Secret is persistent"); + assertEquals(newSecret, hookSecretConfig.getHookSecret().getPlainText(), "Secret correctly stored"); } } \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java b/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java index c65877a15..f252c4dc2 100644 --- a/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/extension/CryptoUtilTest.java @@ -1,40 +1,46 @@ package org.jenkinsci.plugins.github.extension; import hudson.util.Secret; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; import static org.jenkinsci.plugins.github.webhook.GHWebhookSignature.webhookSignature; -import static org.junit.Assert.assertThat; /** * Tests for utility class that deals with crypto/hashing of data. * * @author martinmine */ -public class CryptoUtilTest { +@WithJenkins +class CryptoUtilTest { private static final String SIGNATURE = "85d155c55ed286a300bd1cf124de08d87e914f3a"; private static final String PAYLOAD = "foo"; private static final String SECRET = "bar"; - @ClassRule - public static JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jRule = rule; + } @Test - public void shouldComputeSHA1Signature() throws Exception { + void shouldComputeSHA1Signature() throws Exception { assertThat("signature is valid", webhookSignature( - PAYLOAD, + PAYLOAD, Secret.fromString(SECRET) ).sha1(), equalTo(SIGNATURE)); } @Test - public void shouldMatchSignature() throws Exception { + void shouldMatchSignature() throws Exception { assertThat("signature should match", webhookSignature( - PAYLOAD, + PAYLOAD, Secret.fromString(SECRET) ).matches(SIGNATURE), equalTo(true)); } diff --git a/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java b/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java index 0f0187f2c..18f4c0666 100644 --- a/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriberTest.java @@ -1,9 +1,7 @@ package org.jenkinsci.plugins.github.extension; import hudson.model.Item; -import hudson.model.Job; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.kohsuke.github.GHEvent; import java.util.Set; @@ -15,16 +13,16 @@ /** * @author lanwen (Merkushev Kirill) */ -public class GHEventsSubscriberTest { +class GHEventsSubscriberTest { @Test - public void shouldReturnEmptySetInsteadOfNull() throws Exception { + void shouldReturnEmptySetInsteadOfNull() throws Exception { Set set = GHEventsSubscriber.extractEvents().apply(new NullSubscriber()); assertThat("null should be replaced", set, hasSize(0)); } @Test - public void shouldMatchAgainstEmptySetInsteadOfNull() throws Exception { + void shouldMatchAgainstEmptySetInsteadOfNull() throws Exception { boolean result = GHEventsSubscriber.isInterestedIn(GHEvent.PUSH).apply(new NullSubscriber()); assertThat("null should be replaced", result, is(false)); } diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java index 7a7b0c7b3..ff8a74669 100644 --- a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheCleanupTest.java @@ -1,13 +1,14 @@ package org.jenkinsci.plugins.github.internal; -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import hudson.Functions; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.jenkinsci.plugins.github.test.GHMockRule; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.jenkinsci.plugins.github.test.GitHubMockExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GitHub; import java.io.IOException; @@ -20,43 +21,41 @@ import static java.nio.file.Files.newDirectoryStream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.startsWith; import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.clearRedundantCaches; import static org.jenkinsci.plugins.github.internal.GitHubClientCacheOps.getBaseCacheDir; -import static org.junit.Assume.assumeThat; +import static org.junit.jupiter.api.Assumptions.assumeFalse; /** * @author lanwen (Merkushev Kirill) */ -public class GitHubClientCacheCleanupTest { +@WithJenkins +class GitHubClientCacheCleanupTest { public static final String DEFAULT_CREDS_ID = ""; public static final String CHANGED_CREDS_ID = "id"; - @Rule - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; - @Rule - public GHMockRule github = new GHMockRule(new WireMockRule(wireMockConfig().dynamicPort())).stubUser(); + @RegisterExtension + static GitHubMockExtension github = new GitHubMockExtension(WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort())) + .stubUser(); - @Before - public void setUp() throws Exception { - assumeThat("ignore for windows (dunno how to fix it without win - heed help!)", - Functions.isWindows(), is(false) - ); + @BeforeEach + void setUp(JenkinsRule rule) { + assumeFalse(Functions.isWindows(), "ignore for windows (dunno how to fix it without win - heed help!)"); + jRule = rule; } @Test - public void shouldCreateCachedFolder() throws Exception { + void shouldCreateCachedFolder() throws Exception { makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); it("should create cached dir", 1); } @Test - public void shouldCreateOnlyOneCachedFolderForSameCredsAndApi() throws Exception { + void shouldCreateOnlyOneCachedFolderForSameCredsAndApi() throws Exception { makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); @@ -64,7 +63,7 @@ public void shouldCreateOnlyOneCachedFolderForSameCredsAndApi() throws Exception } @Test - public void shouldCreateCachedFolderForEachCreds() throws Exception { + void shouldCreateCachedFolderForEachCreds() throws Exception { makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); makeCachedRequestWithCredsId(CHANGED_CREDS_ID); @@ -72,7 +71,7 @@ public void shouldCreateCachedFolderForEachCreds() throws Exception { } @Test - public void shouldRemoveCachedDirAfterClean() throws Exception { + void shouldRemoveCachedDirAfterClean() throws Exception { makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); clearRedundantCaches(Collections.emptyList()); @@ -81,7 +80,7 @@ public void shouldRemoveCachedDirAfterClean() throws Exception { } @Test - public void shouldRemoveOnlyNotActiveCachedDirAfterClean() throws Exception { + void shouldRemoveOnlyNotActiveCachedDirAfterClean() throws Exception { makeCachedRequestWithCredsId(DEFAULT_CREDS_ID); makeCachedRequestWithCredsId(CHANGED_CREDS_ID); @@ -95,7 +94,7 @@ public void shouldRemoveOnlyNotActiveCachedDirAfterClean() throws Exception { } @Test - public void shouldRemoveCacheWhichNotEnabled() throws Exception { + void shouldRemoveCacheWhichNotEnabled() throws Exception { makeCachedRequestWithCredsId(CHANGED_CREDS_ID); GitHubServerConfig config = new GitHubServerConfig(CHANGED_CREDS_ID); diff --git a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java index 3aa50f93b..af03c5ead 100644 --- a/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/internal/GitHubClientCacheOpsTest.java @@ -2,12 +2,12 @@ import okhttp3.Cache; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import java.io.File; @@ -23,20 +23,25 @@ /** * @author lanwen (Merkushev Kirill) */ -public class GitHubClientCacheOpsTest { +@WithJenkins +class GitHubClientCacheOpsTest { public static final String CREDENTIALS_ID = "credsid"; public static final String CREDENTIALS_ID_2 = "credsid2"; public static final String CUSTOM_API_URL = "http://api.some.unk/"; - @ClassRule - public static TemporaryFolder tmp = new TemporaryFolder(); + @TempDir + public static File tmp; - @Rule - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jRule = rule; + } @Test - public void shouldPointToSameCacheForOneConfig() throws Exception { + void shouldPointToSameCacheForOneConfig() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); Cache cache1 = toCacheDir().apply(config); @@ -47,7 +52,7 @@ public void shouldPointToSameCacheForOneConfig() throws Exception { } @Test - public void shouldPointToDifferentCachesOnChangedApiPath() throws Exception { + void shouldPointToDifferentCachesOnChangedApiPath() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setApiUrl(CUSTOM_API_URL); @@ -61,7 +66,7 @@ public void shouldPointToDifferentCachesOnChangedApiPath() throws Exception { } @Test - public void shouldPointToDifferentCachesOnChangedCreds() throws Exception { + void shouldPointToDifferentCachesOnChangedCreds() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); GitHubServerConfig config2 = new GitHubServerConfig(CREDENTIALS_ID_2); @@ -74,30 +79,30 @@ public void shouldPointToDifferentCachesOnChangedCreds() throws Exception { @Test @WithoutJenkins - public void shouldNotAcceptFilesInFilter() throws Exception { + void shouldNotAcceptFilesInFilter() throws Exception { assertThat("file should not be accepted", - notInCaches(newHashSet("file")).accept(tmp.newFile().toPath()), is(false)); + notInCaches(newHashSet("file")).accept(File.createTempFile("junit", null, tmp).toPath()), is(false)); } @Test @WithoutJenkins - public void shouldNotAcceptDirsInFilterWithNameFromSet() throws Exception { - File dir = tmp.newFolder(); + void shouldNotAcceptDirsInFilterWithNameFromSet() throws Exception { + File dir = newFolder(tmp, "junit"); assertThat("should not accept folders from set", notInCaches(newHashSet(dir.getName())).accept(dir.toPath()), is(false)); } @Test @WithoutJenkins - public void shouldAcceptDirsInFilterWithNameNotInSet() throws Exception { - File dir = tmp.newFolder(); + void shouldAcceptDirsInFilterWithNameNotInSet() throws Exception { + File dir = newFolder(tmp, "junit"); assertThat("should accept folders not in set", notInCaches(newHashSet(dir.getName() + "abc")).accept(dir.toPath()), is(true)); } @Test @WithoutJenkins - public void shouldReturnEnabledOnCacheGreaterThan0() throws Exception { + void shouldReturnEnabledOnCacheGreaterThan0() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setClientCacheSize(1); @@ -106,7 +111,7 @@ public void shouldReturnEnabledOnCacheGreaterThan0() throws Exception { @Test @WithoutJenkins - public void shouldReturnNotEnabledOnCacheEq0() throws Exception { + void shouldReturnNotEnabledOnCacheEq0() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setClientCacheSize(0); @@ -115,7 +120,7 @@ public void shouldReturnNotEnabledOnCacheEq0() throws Exception { @Test @WithoutJenkins - public void shouldReturnNotEnabledOnCacheLessThan0() throws Exception { + void shouldReturnNotEnabledOnCacheLessThan0() throws Exception { GitHubServerConfig config = new GitHubServerConfig(CREDENTIALS_ID); config.setClientCacheSize(-1); @@ -124,7 +129,14 @@ public void shouldReturnNotEnabledOnCacheLessThan0() throws Exception { @Test @WithoutJenkins - public void shouldHaveEnabledCacheByDefault() throws Exception { + void shouldHaveEnabledCacheByDefault() throws Exception { assertThat("default cache", withEnabledCache().apply(new GitHubServerConfig(CREDENTIALS_ID)), is(true)); } + + private static File newFolder(File root, String... subDirs) { + String subFolder = String.join("/", subDirs); + File result = new File(root, subFolder); + result.mkdirs(); + return result; + } } diff --git a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java index 25fff76e3..c4720205f 100644 --- a/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/migration/MigratorTest.java @@ -7,9 +7,10 @@ import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.jvnet.hudson.test.recipes.LocalData; import java.io.IOException; @@ -30,10 +31,10 @@ /** * @author lanwen (Merkushev Kirill) */ -public class MigratorTest { +@WithJenkins +class MigratorTest { - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; public static final String HOOK_FROM_LOCAL_DATA = "http://some.proxy.example.com/webhook"; public static final String CUSTOM_GH_URL = "http://custom.github.example.com/api/v3"; @@ -41,12 +42,17 @@ public class MigratorTest { public static final String TOKEN2 = "some-oauth-token2"; public static final String TOKEN3 = "some-oauth-token3"; + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; + } + /** * Just ignore malformed hook in old config */ @Test @LocalData - public void shouldNotThrowExcMalformedHookUrlInOldConfig() throws IOException { + void shouldNotThrowExcMalformedHookUrlInOldConfig() throws IOException { FreeStyleProject job = jenkins.createFreeStyleProject(); GitHubPushTrigger trigger = new GitHubPushTrigger(); trigger.start(job, true); @@ -60,7 +66,7 @@ public void shouldNotThrowExcMalformedHookUrlInOldConfig() throws IOException { @Test @LocalData - public void shouldMigrateHookUrl() { + void shouldMigrateHookUrl() { assertThat("in plugin - override", GitHubPlugin.configuration().isOverrideHookUrl(), is(true)); assertThat("in plugin", valueOf(GitHubPlugin.configuration().getHookUrl()), is(HOOK_FROM_LOCAL_DATA)); @@ -70,7 +76,7 @@ public void shouldMigrateHookUrl() { @Test @LocalData - public void shouldMigrateCredentials() throws Exception { + void shouldMigrateCredentials() throws Exception { assertThat("should migrate 3 configs", GitHubPlugin.configuration().getConfigs(), hasSize(3)); assertThat("migrate custom url", GitHubPlugin.configuration().getConfigs(), hasItems( both(withApiUrl(is(CUSTOM_GH_URL))).and(withCredsWithToken(TOKEN2)), @@ -81,8 +87,8 @@ public void shouldMigrateCredentials() throws Exception { @Test @LocalData - public void shouldLoadDataAfterStart() throws Exception { - assertThat("should load 3 configs", GitHubPlugin.configuration().getConfigs(), hasSize(2)); + void shouldLoadDataAfterStart() throws Exception { + assertThat("should load 2 configs", GitHubPlugin.configuration().getConfigs(), hasSize(2)); assertThat("migrate custom url", GitHubPlugin.configuration().getConfigs(), hasItems( withApiUrl(is(CUSTOM_GH_URL)), withApiUrl(is(GITHUB_URL)) @@ -92,7 +98,7 @@ public void shouldLoadDataAfterStart() throws Exception { } @Test - public void shouldConvertCredsToServerConfig() throws Exception { + void shouldConvertCredsToServerConfig() throws Exception { GitHubServerConfig conf = new Migrator().toGHServerConfig() .apply(new Credential("name", CUSTOM_GH_URL, "token")); assertThat(conf, both(withCredsWithToken("token")).and(withApiUrl(is(CUSTOM_GH_URL)))); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java b/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java index 1b13af21a..0e3491cae 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/GitHubCommitStatusSetterTest.java @@ -2,7 +2,7 @@ import com.cloudbees.jenkins.GitHubSetCommitStatusBuilder; import com.github.tomakehurst.wiremock.common.Slf4jNotifier; -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; @@ -11,6 +11,7 @@ import hudson.model.Result; import hudson.plugins.git.Revision; import hudson.plugins.git.util.BuildData; +import jakarta.inject.Inject; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.lib.ObjectId; import org.jenkinsci.plugins.github.config.GitHubPluginConfig; @@ -20,21 +21,19 @@ import org.jenkinsci.plugins.github.status.sources.BuildDataRevisionShaSource; import org.jenkinsci.plugins.github.status.sources.DefaultCommitContextSource; import org.jenkinsci.plugins.github.status.sources.DefaultStatusResultSource; -import org.jenkinsci.plugins.github.test.GHMockRule; -import org.jenkinsci.plugins.github.test.GHMockRule.FixedGHRepoNameTestContributor; -import org.jenkinsci.plugins.github.test.InjectJenkinsMembersRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExternalResource; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; +import org.jenkinsci.plugins.github.test.GitHubMockExtension; +import org.jenkinsci.plugins.github.test.GitHubMockExtension.FixedGHRepoNameTestContributor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TestBuilder; import org.jvnet.hudson.test.TestExtension; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -import javax.inject.Inject; import java.util.Collections; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; @@ -45,49 +44,45 @@ /** * Tests for {@link GitHubSetCommitStatusBuilder}. * - * @author Oleg Nenashev + * @author Oleg Nenashev */ -@RunWith(MockitoJUnitRunner.class) +@WithJenkins +@ExtendWith(MockitoExtension.class) public class GitHubCommitStatusSetterTest { public static final String SOME_SHA = StringUtils.repeat("f", 40); - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) public BuildData data; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) public Revision rev; @Inject public GitHubPluginConfig config; - public JenkinsRule jRule = new JenkinsRule(); + private JenkinsRule jRule; - @Rule - public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); - - @Rule - public GHMockRule github = new GHMockRule( - new WireMockRule( - wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)) - )) + @RegisterExtension + static GitHubMockExtension github = new GitHubMockExtension(WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().notifier(new Slf4jNotifier(true)))) .stubUser() .stubRepo() .stubStatuses(); - @Rule - public ExternalResource prep = new ExternalResource() { - @Override - protected void before() throws Throwable { - when(data.getLastBuiltRevision()).thenReturn(rev); - data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); - when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); - } - }; + @BeforeEach + void before(JenkinsRule rule) throws Throwable { + jRule = rule; + jRule.getInstance().getInjector().injectMembers(this); + + when(data.getLastBuiltRevision()).thenReturn(rev); + data.lastBuild = new hudson.plugins.git.util.Build(rev, rev, 0, Result.SUCCESS); + when(rev.getSha1()).thenReturn(ObjectId.fromString(SOME_SHA)); + } @Test - public void shouldSetGHCommitStatus() throws Exception { + void shouldSetGHCommitStatus() throws Exception { config.getConfigs().add(github.serverConfig()); FreeStyleProject prj = jRule.createFreeStyleProject(); @@ -109,11 +104,11 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen prj.getPublishersList().add(statusSetter); prj.scheduleBuild2(0).get(); - github.service().verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); + github.verify(1, postRequestedFor(urlPathMatching(".*/" + SOME_SHA))); } @Test - public void shouldHandleError() throws Exception { + void shouldHandleError() throws Exception { FreeStyleProject prj = jRule.createFreeStyleProject(); GitHubCommitStatusSetter statusSetter = new GitHubCommitStatusSetter(); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java b/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java index d225e9660..e0aaa945e 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/err/ErrorHandlersTest.java @@ -3,21 +3,21 @@ import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; import static org.mockito.Mockito.verify; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class ErrorHandlersTest { +@ExtendWith(MockitoExtension.class) +class ErrorHandlersTest { @Mock private Run run; @@ -26,7 +26,7 @@ public class ErrorHandlersTest { private TaskListener listener; @Test - public void shouldSetFailureResultStatus() throws Exception { + void shouldSetFailureResultStatus() throws Exception { boolean handled = new ChangingBuildStatusErrorHandler(Result.FAILURE.toString()) .handle(new RuntimeException(), run, listener); @@ -35,7 +35,7 @@ public void shouldSetFailureResultStatus() throws Exception { } @Test - public void shouldSetFailureResultStatusOnUnknownSetup() throws Exception { + void shouldSetFailureResultStatusOnUnknownSetup() throws Exception { boolean handled = new ChangingBuildStatusErrorHandler("") .handle(new RuntimeException(), run, listener); @@ -44,7 +44,7 @@ public void shouldSetFailureResultStatusOnUnknownSetup() throws Exception { } @Test - public void shouldHandleAndDoNothing() throws Exception { + void shouldHandleAndDoNothing() throws Exception { boolean handled = new ShallowAnyErrorHandler().handle(new RuntimeException(), run, listener); assertThat("handling", handled, is(true)); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSourceTest.java index ec46021e7..d27ff4055 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSourceTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/BuildRefBackrefSourceTest.java @@ -1,40 +1,42 @@ package org.jenkinsci.plugins.github.status.sources; -import hudson.model.FreeStyleProject; import hudson.model.Run; import hudson.model.TaskListener; import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.when; /** * @author pupssman (Kalinin Ivan) */ -@RunWith(MockitoJUnitRunner.class) -public class BuildRefBackrefSourceTest { +@WithJenkins +@ExtendWith(MockitoExtension.class) +class BuildRefBackrefSourceTest { - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); + private JenkinsRule jenkinsRule; @Mock(answer = Answers.RETURNS_MOCKS) private TaskListener listener; - @Test + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkinsRule = rule; + } + /** * @throws Exception */ - public void shouldReturnRunAbsoluteUrl() throws Exception { + @Test + void shouldReturnRunAbsoluteUrl() throws Exception { Run run = jenkinsRule.buildAndAssertSuccess(jenkinsRule.createFreeStyleProject()); String result = new BuildRefBackrefSource().get(run, listener); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java index 683d7a037..9f7e1695b 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/ConditionalStatusResultSourceTest.java @@ -6,27 +6,27 @@ import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; import org.jenkinsci.plugins.github.extension.status.misc.ConditionalResult; import org.jenkinsci.plugins.github.status.sources.misc.AnyBuildResult; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHCommitState; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.util.Collections; import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult.betterThanOrEqualTo; -import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class ConditionalStatusResultSourceTest { +@ExtendWith(MockitoExtension.class) +class ConditionalStatusResultSourceTest { @Mock(answer = Answers.RETURNS_MOCKS) private Run run; @@ -35,7 +35,7 @@ public class ConditionalStatusResultSourceTest { private TaskListener listener; @Test - public void shouldReturnPendingByDefault() throws Exception { + void shouldReturnPendingByDefault() throws Exception { GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource(null).get(run, listener); assertThat("state", res.getState(), is(GHCommitState.PENDING)); @@ -43,7 +43,7 @@ public void shouldReturnPendingByDefault() throws Exception { } @Test - public void shouldReturnPendingIfNoMatch() throws Exception { + void shouldReturnPendingIfNoMatch() throws Exception { when(run.getResult()).thenReturn(Result.FAILURE); GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource( @@ -57,7 +57,7 @@ public void shouldReturnPendingIfNoMatch() throws Exception { } @Test - public void shouldReturnFirstMatch() throws Exception { + void shouldReturnFirstMatch() throws Exception { GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource(asList( AnyBuildResult.onAnyResult(GHCommitState.FAILURE, "1"), betterThanOrEqualTo(Result.SUCCESS, GHCommitState.SUCCESS, "2") @@ -68,7 +68,7 @@ public void shouldReturnFirstMatch() throws Exception { } @Test - public void shouldReturnFirstMatch2() throws Exception { + void shouldReturnFirstMatch2() throws Exception { when(run.getResult()).thenReturn(Result.SUCCESS); GitHubStatusResultSource.StatusResult res = new ConditionalStatusResultSource(asList( diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java index d4a93e6c3..c06176aae 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/DefaultStatusResultSourceTest.java @@ -1,20 +1,17 @@ package org.jenkinsci.plugins.github.status.sources; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; + import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import org.jenkinsci.plugins.github.extension.status.GitHubStatusResultSource; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.kohsuke.github.GHCommitState; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -23,11 +20,8 @@ /** * @author lanwen (Merkushev Kirill) */ -@RunWith(DataProviderRunner.class) -public class DefaultStatusResultSourceTest { - - @Rule - public MockitoRule mockitoRule = MockitoJUnit.rule(); +@ExtendWith(MockitoExtension.class) +class DefaultStatusResultSourceTest { @Mock(answer = Answers.RETURNS_MOCKS) private Run run; @@ -35,8 +29,7 @@ public class DefaultStatusResultSourceTest { @Mock(answer = Answers.RETURNS_MOCKS) private TaskListener listener; - @DataProvider - public static Object[][] results() { + static Object[][] results() { return new Object[][]{ {Result.SUCCESS, GHCommitState.SUCCESS}, {Result.UNSTABLE, GHCommitState.FAILURE}, @@ -45,9 +38,9 @@ public static Object[][] results() { }; } - @Test - @UseDataProvider("results") - public void shouldReturnConditionalResult(Result actual, GHCommitState expected) throws Exception { + @ParameterizedTest + @MethodSource("results") + void shouldReturnConditionalResult(Result actual, GHCommitState expected) throws Exception { when(run.getResult()).thenReturn(actual); GitHubStatusResultSource.StatusResult result = new DefaultStatusResultSource().get(run, listener); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java index 7bda2012e..2f7d840f5 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredRepositorySourceTest.java @@ -2,25 +2,25 @@ import hudson.model.Run; import hudson.model.TaskListener; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHRepository; import org.mockito.Answers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.io.PrintStream; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -@RunWith(MockitoJUnitRunner.class) -public class ManuallyEnteredRepositorySourceTest { +@ExtendWith(MockitoExtension.class) +class ManuallyEnteredRepositorySourceTest { @Mock(answer = Answers.RETURNS_MOCKS) private Run run; @@ -31,7 +31,7 @@ public class ManuallyEnteredRepositorySourceTest { private PrintStream logger; @Test - public void nullName() { + void nullName() { ManuallyEnteredRepositorySource instance = spy(new ManuallyEnteredRepositorySource("a")); doReturn(logger).when(listener).getLogger(); List repos = instance.repos(run, listener); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java index b583fd113..14e606dd2 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/ManuallyEnteredSourcesTest.java @@ -3,21 +3,21 @@ import hudson.EnvVars; import hudson.model.Run; import hudson.model.TaskListener; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Answers; -import org.mockito.Matchers; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class ManuallyEnteredSourcesTest { public static final String EXPANDED = "expanded"; @@ -32,27 +32,27 @@ public class ManuallyEnteredSourcesTest { @Test - public void shouldExpandContext() throws Exception { + void shouldExpandContext() throws Exception { when(run.getEnvironment(listener)).thenReturn(env); - when(env.expand(Matchers.anyString())).thenReturn(EXPANDED); + when(env.expand(ArgumentMatchers.anyString())).thenReturn(EXPANDED); String context = new ManuallyEnteredCommitContextSource("").context(run, listener); assertThat(context, equalTo(EXPANDED)); } @Test - public void shouldExpandSha() throws Exception { + void shouldExpandSha() throws Exception { when(run.getEnvironment(listener)).thenReturn(env); - when(env.expand(Matchers.anyString())).thenReturn(EXPANDED); + when(env.expand(ArgumentMatchers.anyString())).thenReturn(EXPANDED); String context = new ManuallyEnteredShaSource("").get(run, listener); assertThat(context, equalTo(EXPANDED)); } @Test - public void shouldExpandBackref() throws Exception { + void shouldExpandBackref() throws Exception { when(run.getEnvironment(listener)).thenReturn(env); - when(env.expand(Matchers.anyString())).thenReturn(EXPANDED); + when(env.expand(ArgumentMatchers.anyString())).thenReturn(EXPANDED); String context = new ManuallyEnteredBackrefSource("").get(run, listener); assertThat(context, equalTo(EXPANDED)); diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java index 8b904b06a..145a24266 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/AnyBuildResultTest.java @@ -1,29 +1,29 @@ package org.jenkinsci.plugins.github.status.sources.misc; import hudson.model.Run; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHCommitState; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.verifyNoMoreInteractions; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class AnyBuildResultTest { +@ExtendWith(MockitoExtension.class) +class AnyBuildResultTest { @Mock private Run run; @Test - public void shouldMatchEveryTime() throws Exception { + void shouldMatchEveryTime() throws Exception { boolean matches = AnyBuildResult.onAnyResult(GHCommitState.ERROR, "").matches(run); - - assertTrue("matching", matches); + + assertTrue(matches, "matching"); verifyNoMoreInteractions(run); } diff --git a/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java index ff5c13f5d..75cd588ea 100644 --- a/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/status/sources/misc/BetterThanOrEqualBuildResultTest.java @@ -1,37 +1,29 @@ package org.jenkinsci.plugins.github.status.sources.misc; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; import hudson.model.Result; import hudson.model.Run; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.kohsuke.github.GHCommitState; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.jenkinsci.plugins.github.status.sources.misc.BetterThanOrEqualBuildResult.betterThanOrEqualTo; -import static org.junit.Assert.assertThat; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(DataProviderRunner.class) -public class BetterThanOrEqualBuildResultTest { - - @Rule - public MockitoRule mockitoRule = MockitoJUnit.rule(); +@ExtendWith(MockitoExtension.class) +class BetterThanOrEqualBuildResultTest { @Mock private Run run; - @DataProvider - public static Object[][] results() { + static Object[][] results() { return new Object[][]{ {Result.SUCCESS, Result.SUCCESS, true}, {Result.UNSTABLE, Result.UNSTABLE, true}, @@ -44,9 +36,9 @@ public static Object[][] results() { }; } - @Test - @UseDataProvider("results") - public void shouldMatch(Result defined, Result real, boolean expect) throws Exception { + @ParameterizedTest + @MethodSource("results") + void shouldMatch(Result defined, Result real, boolean expect) throws Exception { Mockito.when(run.getResult()).thenReturn(real); boolean matched = betterThanOrEqualTo(defined, GHCommitState.FAILURE, "").matches(run); diff --git a/src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java b/src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java deleted file mode 100644 index d0c2709e5..000000000 --- a/src/test/java/org/jenkinsci/plugins/github/test/GHMockRule.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.jenkinsci.plugins.github.test; - -import com.cloudbees.jenkins.GitHubRepositoryName; -import com.cloudbees.jenkins.GitHubRepositoryNameContributor; -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import hudson.model.Item; -import hudson.model.Job; -import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; -import static java.lang.String.format; -import static wiremock.org.mortbay.jetty.HttpStatus.ORDINAL_201_Created; - -/** - * Mocks GitHub on localhost with some predefined methods - * - * @author lanwen (Merkushev Kirill) - */ -public class GHMockRule implements TestRule { - - /** - * This repo is used in resource files - */ - public static final GitHubRepositoryName REPO = new GitHubRepositoryName("localhost", "org", "repo"); - - /** - * Wiremock service itself. You can interact with it directly by {@link #service()} method - */ - private WireMockRule service; - - /** - * List of additional stubs. Launched after wiremock has been started - */ - private List setups = new ArrayList<>(); - - public GHMockRule(WireMockRule mocked) { - this.service = mocked; - } - - /** - * @return wiremock rule - */ - public WireMockRule service() { - return service; - } - - /** - * Ready-to-use global config with wiremock service. Just add it to plugin config - * {@code GitHubPlugin.configuration().getConfigs().add(github.serverConfig());} - * - * @return part of global plugin config - */ - public GitHubServerConfig serverConfig() { - GitHubServerConfig conf = new GitHubServerConfig("creds"); - conf.setApiUrl("http://localhost:" + service().port()); - return conf; - } - - /** - * Main method of rule. Firstly starts wiremock, then run predefined setups - */ - @Override - public Statement apply(final Statement base, Description description) { - return service.apply(new Statement() { - @Override - public void evaluate() throws Throwable { - for (Runnable callable : setups) { - callable.run(); - } - base.evaluate(); - } - }, description); - } - - /** - * Stubs /user response with predefined content - * - * More info: https://developer.github.com/v3/users/#get-the-authenticated-user - */ - public GHMockRule stubUser() { - return addSetup(new Runnable() { - @Override - public void run() { - service().stubFor(get(urlPathEqualTo("/user")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json; charset=utf-8") - .withBody(classpath(GHMockRule.class, "user.json")))); - } - }); - } - - /** - * Stubs /repos/org/repo response with predefined content - * - * More info: https://developer.github.com/v3/repos/#get - */ - public GHMockRule stubRepo() { - return addSetup(new Runnable() { - @Override - public void run() { - String repo = format("/repos/%s/%s", REPO.getUserName(), REPO.getRepositoryName()); - service().stubFor( - get(urlPathMatching(repo)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json; charset=utf-8") - .withBody(classpath(GHMockRule.class, "repos-repo.json")))); - } - }); - } - - /** - * Returns 201 CREATED on POST to statuses endpoint (but without content) - * - * More info: https://developer.github.com/v3/repos/statuses/ - */ - public GHMockRule stubStatuses() { - return addSetup(new Runnable() { - @Override - public void run() { - service().stubFor( - post(urlPathMatching( - format("/repos/%s/%s/statuses/.*", REPO.getUserName(), REPO.getRepositoryName())) - ).willReturn(aResponse().withStatus(ORDINAL_201_Created))); - } - }); - } - - /** - * When we call one of predefined stub* methods, wiremock is not not started yet, so we need to create a closure - * - * @param setup closure to setup wiremock - */ - private GHMockRule addSetup(Runnable setup) { - setups.add(setup); - return this; - } - - /** - * Adds predefined repo to list which job can return. This is useful to avoid SCM usage. - * - * {@code @TestExtension - * public static final FixedGHRepoNameTestContributor CONTRIBUTOR = new FixedGHRepoNameTestContributor(); - * } - */ - public static class FixedGHRepoNameTestContributor extends GitHubRepositoryNameContributor { - @Override - public void parseAssociatedNames(Item job, Collection result) { - result.add(GHMockRule.REPO); - } - } -} diff --git a/src/test/java/org/jenkinsci/plugins/github/test/GitHubMockExtension.java b/src/test/java/org/jenkinsci/plugins/github/test/GitHubMockExtension.java new file mode 100644 index 000000000..fc5687a9f --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/test/GitHubMockExtension.java @@ -0,0 +1,123 @@ +package org.jenkinsci.plugins.github.test; + +import com.cloudbees.jenkins.GitHubRepositoryName; +import com.cloudbees.jenkins.GitHubRepositoryNameContributor; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import hudson.model.Item; +import org.jenkinsci.plugins.github.config.GitHubServerConfig; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; +import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_CREATED; +import static java.net.HttpURLConnection.HTTP_OK; + +/** + * Mocks GitHub on localhost with some predefined methods + * + * @author lanwen (Merkushev Kirill) + */ +public class GitHubMockExtension extends WireMockExtension { + + /** + * This repo is used in resource files + */ + public static final GitHubRepositoryName REPO = new GitHubRepositoryName("localhost", "org", "repo"); + + /** + * List of additional stubs. Launched after wiremock has been started + */ + private final List setups = new ArrayList<>(); + + public GitHubMockExtension(Builder builder) { + super(builder); + } + + @Override + protected void onBeforeEach(WireMockRuntimeInfo wireMockRuntimeInfo) { + super.onBeforeAll(wireMockRuntimeInfo); + + for (Runnable setup : setups) { + setup.run(); + } + } + + /** + * Ready-to-use global config with wiremock service. Just add it to plugin config + * {@code GitHubPlugin.configuration().getConfigs().add(github.serverConfig());} + * + * @return part of global plugin config + */ + public GitHubServerConfig serverConfig() { + GitHubServerConfig conf = new GitHubServerConfig("creds"); + conf.setApiUrl("http://localhost:" + getPort()); + return conf; + } + + /** + * Stubs /user response with predefined content + *

+ * More info: https://developer.github.com/v3/users/#get-the-authenticated-user + */ + public GitHubMockExtension stubUser() { + setups.add(() -> + stubFor(get(urlPathEqualTo("/user")) + .willReturn(aResponse() + .withStatus(HTTP_OK) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody(classpath(GitHubMockExtension.class, "user.json"))))); + return this; + } + + /** + * Stubs /repos/org/repo response with predefined content + *

+ * More info: https://developer.github.com/v3/repos/#get + */ + public GitHubMockExtension stubRepo() { + setups.add(() -> + stubFor(get(urlPathMatching(format("/repos/%s/%s", REPO.getUserName(), REPO.getRepositoryName()))) + .willReturn(aResponse() + .withStatus(HTTP_OK) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody(classpath(GitHubMockExtension.class, "repos-repo.json"))))); + return this; + } + + /** + * Returns 201 CREATED on POST to statuses endpoint (but without content) + *

+ * More info: https://developer.github.com/v3/repos/statuses/ + */ + public GitHubMockExtension stubStatuses() { + setups.add(() -> + stubFor(post(urlPathMatching(format("/repos/%s/%s/statuses/.*", REPO.getUserName(), REPO.getRepositoryName()))) + .willReturn(aResponse() + .withStatus(HTTP_CREATED)))); + return this; + } + + /** + * Adds predefined repo to list which job can return. This is useful to avoid SCM usage. + *

+ * {@code @TestExtension + * public static final FixedGHRepoNameTestContributor CONTRIBUTOR = new FixedGHRepoNameTestContributor(); + * } + */ + public static class FixedGHRepoNameTestContributor extends GitHubRepositoryNameContributor { + @Override + public void parseAssociatedNames(Item job, Collection result) { + result.add(GitHubMockExtension.REPO); + } + } + +} diff --git a/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java b/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java index 6763e8dd0..2a391af6e 100644 --- a/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java +++ b/src/test/java/org/jenkinsci/plugins/github/test/GitHubServerConfigMatcher.java @@ -5,10 +5,6 @@ import org.hamcrest.FeatureMatcher; import org.hamcrest.Matcher; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; import static org.hamcrest.Matchers.is; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.tokenFor; @@ -17,7 +13,6 @@ * @author lanwen (Merkushev Kirill) */ public final class GitHubServerConfigMatcher { - private static final Logger LOG = LoggerFactory.getLogger(GitHubServerConfigMatcher.class); private GitHubServerConfigMatcher() { } diff --git a/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java b/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java index 0d6d7e3db..b2d7d8960 100644 --- a/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java +++ b/src/test/java/org/jenkinsci/plugins/github/test/HookSecretHelper.java @@ -27,7 +27,7 @@ private HookSecretHelper() { /** * Stores the secret and sets it as the current hook secret. - * + * * @param config where to save * @param secretText The secret/key. */ @@ -56,13 +56,13 @@ public void run() { config.setHookSecretConfigs(Collections.singletonList(new HookSecretConfig(credentials.getId()))); } - + /** * Stores the secret and sets it as the current hook secret. * @param secretText The secret/key. */ public static void storeSecret(final String secretText) { - storeSecretIn(Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class), secretText); + storeSecretIn(Jenkins.get().getDescriptorByType(GitHubPluginConfig.class), secretText); } /** @@ -78,6 +78,6 @@ public static void removeSecretIn(GitHubPluginConfig config) { * Unsets the current hook secret. */ public static void removeSecret() { - removeSecretIn(Jenkins.getInstance().getDescriptorByType(GitHubPluginConfig.class)); + removeSecretIn(Jenkins.get().getDescriptorByType(GitHubPluginConfig.class)); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java b/src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java deleted file mode 100644 index ae0127783..000000000 --- a/src/test/java/org/jenkinsci/plugins/github/test/InjectJenkinsMembersRule.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.jenkinsci.plugins.github.test; - -import org.junit.rules.ExternalResource; -import org.jvnet.hudson.test.JenkinsRule; - -/** - * Helpful class to make possible usage of - * {@code @Inject - * public GitHubPluginConfig config; - * } - * - * in test fields instead of static calls {@link org.jenkinsci.plugins.github.GitHubPlugin#configuration()} - * - * See {@link com.cloudbees.jenkins.GitHubSetCommitStatusBuilderTest} for example - * Should be used after JenkinsRule initialized - * - * {@code public RuleChain chain = RuleChain.outerRule(jRule).around(new InjectJenkinsMembersRule(jRule, this)); } - * - * @author lanwen (Merkushev Kirill) - */ -public class InjectJenkinsMembersRule extends ExternalResource { - - private JenkinsRule jRule; - private Object instance; - - /** - * @param jRule Jenkins rule - * @param instance test class instance - */ - public InjectJenkinsMembersRule(JenkinsRule jRule, Object instance) { - this.jRule = jRule; - this.instance = instance; - } - - @Override - protected void before() throws Throwable { - jRule.getInstance().getInjector().injectMembers(instance); - } -} diff --git a/src/test/java/org/jenkinsci/plugins/github/util/BuildDataHelperTest.java b/src/test/java/org/jenkinsci/plugins/github/util/BuildDataHelperTest.java index 0f58cc9e0..0cf91e16b 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/BuildDataHelperTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/BuildDataHelperTest.java @@ -1,10 +1,8 @@ package org.jenkinsci.plugins.github.util; import hudson.plugins.git.util.BuildData; - -import org.junit.Test; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import java.util.ArrayList; @@ -20,16 +18,16 @@ /** * @author Manuel de la Peña */ -@RunWith(Enclosed.class) -public class BuildDataHelperTest { +class BuildDataHelperTest { - public static class WhenBuildingRegularJobs { + @Nested + class WhenBuildingRegularJobs { private static final String GITHUB_USERNAME = "user1"; @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProject() throws Exception { + void shouldCalculateDataBuildFromProject() throws Exception { BuildData projectBuildData = new BuildData(); projectBuildData.remoteUrls = new HashSet<>(); @@ -48,7 +46,7 @@ public void shouldCalculateDataBuildFromProject() throws Exception { @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProjectWithTwoBuildDatas() throws Exception { + void shouldCalculateDataBuildFromProjectWithTwoBuildDatas() throws Exception { BuildData sharedLibBuildData = new BuildData(); sharedLibBuildData.remoteUrls = new HashSet<>(); @@ -74,7 +72,7 @@ public void shouldCalculateDataBuildFromProjectWithTwoBuildDatas() throws Except @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProjectWithEmptyBuildDatas() throws Exception { + void shouldCalculateDataBuildFromProjectWithEmptyBuildDatas() throws Exception { BuildData buildData = BuildDataHelper.calculateBuildData( "master", "project/master", Collections.EMPTY_LIST); @@ -83,7 +81,7 @@ public void shouldCalculateDataBuildFromProjectWithEmptyBuildDatas() throws Exce @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProjectWithNullBuildDatas() throws Exception { + void shouldCalculateDataBuildFromProjectWithNullBuildDatas() throws Exception { BuildData buildData = BuildDataHelper.calculateBuildData( "master", "project/master", null); @@ -92,13 +90,14 @@ public void shouldCalculateDataBuildFromProjectWithNullBuildDatas() throws Excep } - public static class WhenBuildingOrganizationJobs { + @Nested + class WhenBuildingOrganizationJobs { private static final String ORGANIZATION_NAME = "Organization"; @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProject() throws Exception { + void shouldCalculateDataBuildFromProject() throws Exception { BuildData projectBuildData = new BuildData(); projectBuildData.remoteUrls = new HashSet<>(); @@ -117,7 +116,7 @@ public void shouldCalculateDataBuildFromProject() throws Exception { @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProjectWithTwoBuildDatas() throws Exception { + void shouldCalculateDataBuildFromProjectWithTwoBuildDatas() throws Exception { BuildData sharedLibBuildData = new BuildData(); sharedLibBuildData.remoteUrls = new HashSet<>(); @@ -143,7 +142,7 @@ public void shouldCalculateDataBuildFromProjectWithTwoBuildDatas() throws Except @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProjectWithEmptyBuildDatas() throws Exception { + void shouldCalculateDataBuildFromProjectWithEmptyBuildDatas() throws Exception { BuildData buildData = BuildDataHelper.calculateBuildData( "master", ORGANIZATION_NAME + "/project/master", Collections.EMPTY_LIST); @@ -152,7 +151,7 @@ public void shouldCalculateDataBuildFromProjectWithEmptyBuildDatas() throws Exce @Test @Issue("JENKINS-53149") - public void shouldCalculateDataBuildFromProjectWithNullBuildDatas() throws Exception { + void shouldCalculateDataBuildFromProjectWithNullBuildDatas() throws Exception { BuildData buildData = BuildDataHelper.calculateBuildData( "master", ORGANIZATION_NAME + "/project/master", null); diff --git a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java index 04de9b1bb..93e8a2b65 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/JobInfoHelpersTest.java @@ -4,28 +4,34 @@ import hudson.model.FreeStyleProject; import hudson.model.Item; import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isAlive; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.isBuildable; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.triggerFrom; import static org.jenkinsci.plugins.github.util.JobInfoHelpers.withTrigger; -import static org.junit.Assert.assertThat; /** * @author lanwen (Merkushev Kirill) */ -public class JobInfoHelpersTest { +@WithJenkins +class JobInfoHelpersTest { - @ClassRule - public static JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; + } @Test - public void shouldMatchForProjectWithTrigger() throws Exception { + void shouldMatchForProjectWithTrigger() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); prj.addTrigger(new GitHubPushTrigger()); @@ -33,7 +39,7 @@ public void shouldMatchForProjectWithTrigger() throws Exception { } @Test - public void shouldSeeProjectWithTriggerIsAliveForCleaner() throws Exception { + void shouldSeeProjectWithTriggerIsAliveForCleaner() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); prj.addTrigger(new GitHubPushTrigger()); @@ -41,31 +47,31 @@ public void shouldSeeProjectWithTriggerIsAliveForCleaner() throws Exception { } @Test - public void shouldNotMatchProjectWithoutTrigger() throws Exception { + void shouldNotMatchProjectWithoutTrigger() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); assertThat("without trigger", withTrigger(GitHubPushTrigger.class).apply(prj), is(false)); } @Test - public void shouldNotMatchNullProject() throws Exception { + void shouldNotMatchNullProject() throws Exception { assertThat("null project", withTrigger(GitHubPushTrigger.class).apply(null), is(false)); } @Test - public void shouldReturnNotBuildableOnNullProject() throws Exception { + void shouldReturnNotBuildableOnNullProject() throws Exception { assertThat("null project", isBuildable().apply(null), is(false)); } @Test - public void shouldSeeProjectWithoutTriggerIsNotAliveForCleaner() throws Exception { + void shouldSeeProjectWithoutTriggerIsNotAliveForCleaner() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); assertThat("without trigger", isAlive().apply(prj), is(false)); } @Test - public void shouldGetTriggerFromAbstractProject() throws Exception { + void shouldGetTriggerFromAbstractProject() throws Exception { GitHubPushTrigger trigger = new GitHubPushTrigger(); FreeStyleProject prj = jenkins.createFreeStyleProject(); @@ -75,7 +81,7 @@ public void shouldGetTriggerFromAbstractProject() throws Exception { } @Test - public void shouldGetTriggerFromWorkflow() throws Exception { + void shouldGetTriggerFromWorkflow() throws Exception { GitHubPushTrigger trigger = new GitHubPushTrigger(); WorkflowJob job = jenkins.getInstance().createProject(WorkflowJob.class, "Test Workflow"); job.addTrigger(trigger); @@ -84,7 +90,7 @@ public void shouldGetTriggerFromWorkflow() throws Exception { } @Test - public void shouldNotGetTriggerWhenNoOne() throws Exception { + void shouldNotGetTriggerWhenNoOne() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); assertThat("without trigger in project", triggerFrom((Item) prj, GitHubPushTrigger.class), nullValue()); diff --git a/src/test/java/org/jenkinsci/plugins/github/util/XSSApiTest.java b/src/test/java/org/jenkinsci/plugins/github/util/XSSApiTest.java index 4ce33af75..e1bc391e7 100644 --- a/src/test/java/org/jenkinsci/plugins/github/util/XSSApiTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/util/XSSApiTest.java @@ -1,10 +1,7 @@ package org.jenkinsci.plugins.github.util; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static java.lang.String.format; import static org.hamcrest.MatcherAssert.assertThat; @@ -13,11 +10,9 @@ /** * @author lanwen (Merkushev Kirill) */ -@RunWith(DataProviderRunner.class) -public class XSSApiTest { +class XSSApiTest { - @DataProvider - public static Object[][] links() { + static Object[][] links() { return new Object[][]{ new Object[]{"javascript:alert(1);//", ""}, new Object[]{"javascript:alert(1)://", ""}, @@ -37,9 +32,9 @@ public static Object[][] links() { }; } - @Test - @UseDataProvider("links") - public void shouldSanitizeUrl(String url, String expected) throws Exception { + @ParameterizedTest + @MethodSource("links") + void shouldSanitizeUrl(String url, String expected) throws Exception { assertThat(format("For %s", url), XSSApi.asValidHref(url), is(expected)); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java index d013196d6..ee350a301 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventHeaderTest.java @@ -1,36 +1,37 @@ package org.jenkinsci.plugins.github.webhook; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.kohsuke.github.GHEvent; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class GHEventHeaderTest { public static final String STRING_PUSH_HEADER = "push"; public static final String PARAM_NAME = "event"; public static final String UNKNOWN_EVENT = "unkn"; - + @Mock - private StaplerRequest req; + private StaplerRequest2 req; @Mock private GHEventHeader ann; @Test - public void shouldReturnParsedPushHeader() throws Exception { + void shouldReturnParsedPushHeader() throws Exception { when(req.getHeader(GHEventHeader.PayloadHandler.EVENT_HEADER)).thenReturn(STRING_PUSH_HEADER); Object event = new GHEventHeader.PayloadHandler().parse(req, ann, GHEvent.class, PARAM_NAME); @@ -39,22 +40,23 @@ public void shouldReturnParsedPushHeader() throws Exception { } @Test - public void shouldReturnNullOnEmptyHeader() throws Exception { + void shouldReturnNullOnEmptyHeader() throws Exception { Object event = new GHEventHeader.PayloadHandler().parse(req, ann, GHEvent.class, PARAM_NAME); assertThat("event with empty header", event, nullValue()); } @Test - public void shouldReturnNullOnUnknownEventHeader() throws Exception { + void shouldReturnNullOnUnknownEventHeader() throws Exception { when(req.getHeader(GHEventHeader.PayloadHandler.EVENT_HEADER)).thenReturn(UNKNOWN_EVENT); Object event = new GHEventHeader.PayloadHandler().parse(req, ann, GHEvent.class, PARAM_NAME); assertThat("event with unknown event header", event, nullValue()); } - - @Test(expected = IllegalArgumentException.class) - public void shouldThrowExcOnWrongTypeOfHeader() throws Exception { - new GHEventHeader.PayloadHandler().parse(req, ann, String.class, PARAM_NAME); + + @Test + void shouldThrowExcOnWrongTypeOfHeader() { + assertThrows(IllegalArgumentException.class, () -> + new GHEventHeader.PayloadHandler().parse(req, ann, String.class, PARAM_NAME)); } } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java index f0d0accfb..3c0b1a17e 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/GHEventPayloadTest.java @@ -1,11 +1,11 @@ package org.jenkinsci.plugins.github.webhook; import com.cloudbees.jenkins.GitHubWebHookFullTest; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.kohsuke.stapler.StaplerRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.stapler.StaplerRequest2; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -16,7 +16,7 @@ /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) public class GHEventPayloadTest { public static final String NOT_EMPTY_PAYLOAD_CONTENT = "{}"; @@ -24,13 +24,13 @@ public class GHEventPayloadTest { public static final String UNKNOWN_CONTENT_TYPE = "text/plain"; @Mock - private StaplerRequest req; + private StaplerRequest2 req; @Mock private GHEventPayload ann; @Test - public void shouldReturnPayloadFromForm() throws Exception { + void shouldReturnPayloadFromForm() throws Exception { when(req.getContentType()).thenReturn(GitHubWebHookFullTest.FORM); when(req.getParameter(PARAM_NAME)).thenReturn(NOT_EMPTY_PAYLOAD_CONTENT); Object payload = new GHEventPayload.PayloadHandler().parse(req, ann, String.class, PARAM_NAME); @@ -40,7 +40,7 @@ public void shouldReturnPayloadFromForm() throws Exception { } @Test - public void shouldReturnNullOnUnknownContentType() throws Exception { + void shouldReturnNullOnUnknownContentType() throws Exception { when(req.getContentType()).thenReturn(UNKNOWN_CONTENT_TYPE); Object payload = new GHEventPayload.PayloadHandler().parse(req, ann, String.class, PARAM_NAME); diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignatureSHA256Test.java b/src/test/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignatureSHA256Test.java new file mode 100644 index 000000000..e818d5a5d --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/GHWebhookSignatureSHA256Test.java @@ -0,0 +1,92 @@ +package org.jenkinsci.plugins.github.webhook; + +import hudson.util.Secret; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * Tests for SHA-256 functionality in {@link GHWebhookSignature}. + * + * @since 1.45.0 + */ +class GHWebhookSignatureSHA256Test { + + private static final String SECRET_CONTENT = "It's a Secret to Everybody"; + private static final String PAYLOAD = "Hello, World!"; + // Expected SHA-256 signature based on GitHub's documentation + private static final String EXPECTED_SHA256_DIGEST = "757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17"; + + @Test + void shouldComputeCorrectSHA256Signature() { + Secret secret = Secret.fromString(SECRET_CONTENT); + GHWebhookSignature signature = GHWebhookSignature.webhookSignature(PAYLOAD, secret); + + String computed = signature.sha256(); + + assertThat("SHA-256 signature should match expected value", + computed, equalTo(EXPECTED_SHA256_DIGEST)); + } + + @Test + void shouldValidateSHA256SignatureCorrectly() { + Secret secret = Secret.fromString(SECRET_CONTENT); + GHWebhookSignature signature = GHWebhookSignature.webhookSignature(PAYLOAD, secret); + + boolean isValid = signature.matches(EXPECTED_SHA256_DIGEST, SignatureAlgorithm.SHA256); + + assertThat("Valid SHA-256 signature should be accepted", isValid, equalTo(true)); + } + + @Test + void shouldRejectInvalidSHA256Signature() { + Secret secret = Secret.fromString(SECRET_CONTENT); + GHWebhookSignature signature = GHWebhookSignature.webhookSignature(PAYLOAD, secret); + + String invalidDigest = "invalid_signature_digest"; + boolean isValid = signature.matches(invalidDigest, SignatureAlgorithm.SHA256); + + assertThat("Invalid SHA-256 signature should be rejected", isValid, equalTo(false)); + } + + @Test + void shouldRejectSHA1SignatureWhenExpectingSHA256() { + String secretContent = "test-secret"; + Secret secret = Secret.fromString(secretContent); + GHWebhookSignature signature = GHWebhookSignature.webhookSignature(PAYLOAD, secret); + + // Get SHA-1 digest but try to validate as SHA-256 + String sha1Digest = signature.sha1(); + boolean isValid = signature.matches(sha1Digest, SignatureAlgorithm.SHA256); + + assertThat("SHA-1 signature should be rejected when expecting SHA-256", + isValid, equalTo(false)); + } + + @Test + void shouldHandleDifferentPayloads() { + Secret secret = Secret.fromString(SECRET_CONTENT); + String payload1 = "payload1"; + String payload2 = "payload2"; + + GHWebhookSignature signature1 = GHWebhookSignature.webhookSignature(payload1, secret); + GHWebhookSignature signature2 = GHWebhookSignature.webhookSignature(payload2, secret); + + String digest1 = signature1.sha256(); + String digest2 = signature2.sha256(); + + assertThat("Different payloads should produce different signatures", + digest1.equals(digest2), equalTo(false)); + + // Each signature should validate its own payload + assertThat("Signature 1 should validate payload 1", + signature1.matches(digest1, SignatureAlgorithm.SHA256), equalTo(true)); + assertThat("Signature 2 should validate payload 2", + signature2.matches(digest2, SignatureAlgorithm.SHA256), equalTo(true)); + + // Cross-validation should fail + assertThat("Signature 1 should not validate payload 2's digest", + signature1.matches(digest2, SignatureAlgorithm.SHA256), equalTo(false)); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java index 0d9b787cb..b51d2f0fd 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/RequirePostWithGHHookPayloadTest.java @@ -1,141 +1,150 @@ package org.jenkinsci.plugins.github.webhook; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GHEvent; -import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerRequest2; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import java.lang.reflect.InvocationTargetException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecret; import static org.jenkinsci.plugins.github.test.HookSecretHelper.removeSecret; +import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecret; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) -public class RequirePostWithGHHookPayloadTest { +@WithJenkins +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RequirePostWithGHHookPayloadTest { private static final String SECRET_CONTENT = "secret"; private static final String PAYLOAD = "sample payload"; @Mock - private StaplerRequest req; + private StaplerRequest2 req; - @Rule - public JenkinsRule jenkinsRule = new JenkinsRule(); + private JenkinsRule jenkinsRule; @Spy private RequirePostWithGHHookPayload.Processor processor; - @Before - public void setSecret() { + @BeforeEach + void setUp(JenkinsRule rule) { + jenkinsRule = rule; storeSecret(SECRET_CONTENT); } @Test - public void shouldPassOnlyPost() throws Exception { + void shouldPassOnlyPost() throws Exception { when(req.getMethod()).thenReturn("POST"); new RequirePostWithGHHookPayload.Processor().shouldBePostMethod(req); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnNotPost() throws Exception { + @Test + void shouldNotPassOnNotPost() { when(req.getMethod()).thenReturn("GET"); - new RequirePostWithGHHookPayload.Processor().shouldBePostMethod(req); + assertThrows(InvocationTargetException.class, () -> + new RequirePostWithGHHookPayload.Processor().shouldBePostMethod(req)); } @Test - public void shouldPassOnGHEventAndNotBlankPayload() throws Exception { + void shouldPassOnGHEventAndNotBlankPayload() throws Exception { new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( new Object[]{GHEvent.PUSH, "{}"}); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnNullGHEventAndNotBlankPayload() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[]{null, "{}"}); + @Test + void shouldNotPassOnNullGHEventAndNotBlankPayload() { + assertThrows(InvocationTargetException.class, () -> + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{null, "{}"})); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnGHEventAndBlankPayload() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[]{GHEvent.PUSH, " "}); + @Test + void shouldNotPassOnGHEventAndBlankPayload() { + assertThrows(InvocationTargetException.class, () -> + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{GHEvent.PUSH, " "})); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnNulls() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[]{null, null}); + @Test + void shouldNotPassOnNulls() { + assertThrows(InvocationTargetException.class, () -> + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{null, null})); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnGreaterCountOfArgs() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[]{GHEvent.PUSH, "{}", " "} - ); + @Test + void shouldNotPassOnGreaterCountOfArgs() { + assertThrows(InvocationTargetException.class, () -> + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{GHEvent.PUSH, "{}", " "} + )); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnLessCountOfArgs() throws Exception { - new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( - new Object[]{GHEvent.PUSH} - ); + @Test + void shouldNotPassOnLessCountOfArgs() { + assertThrows(InvocationTargetException.class, () -> + new RequirePostWithGHHookPayload.Processor().shouldContainParseablePayload( + new Object[]{GHEvent.PUSH} + )); } @Test @Issue("JENKINS-37481") - public void shouldPassOnAbsentSignatureInRequestIfSecretIsNotConfigured() throws Exception { - doReturn(PAYLOAD).when(processor).payloadFrom(req, null); + void shouldPassOnAbsentSignatureInRequestIfSecretIsNotConfigured() throws Exception { removeSecret(); processor.shouldProvideValidSignature(req, null); } - @Test(expected = InvocationTargetException.class) + @Test @Issue("JENKINS-48012") - public void shouldNotPassOnAbsentSignatureInRequest() throws Exception { - doReturn(PAYLOAD).when(processor).payloadFrom(req, null); - - processor.shouldProvideValidSignature(req, null); + void shouldNotPassOnAbsentSignatureInRequest() { + assertThrows(InvocationTargetException.class, () -> + processor.shouldProvideValidSignature(req, null)); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnInvalidSignature() throws Exception { + @Test + void shouldNotPassOnInvalidSignature() { final String signature = "sha1=a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"; - when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); doReturn(PAYLOAD).when(processor).payloadFrom(req, null); - - processor.shouldProvideValidSignature(req, null); + assertThrows(InvocationTargetException.class, () -> + processor.shouldProvideValidSignature(req, null)); } - @Test(expected = InvocationTargetException.class) - public void shouldNotPassOnMalformedSignature() throws Exception { + @Test + void shouldNotPassOnMalformedSignature() { final String signature = "49d5f5cf800a81f257324912969a2d325d13d3fc"; - when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); doReturn(PAYLOAD).when(processor).payloadFrom(req, null); - - processor.shouldProvideValidSignature(req, null); + assertThrows(InvocationTargetException.class, () -> + processor.shouldProvideValidSignature(req, null)); } @Test - public void shouldPassWithValidSignature() throws Exception { + void shouldPassWithValidSignature() throws Exception { final String signature = "sha1=49d5f5cf800a81f257324912969a2d325d13d3fc"; + final String signature256 = "sha256=569beaec8ea1c9deccec283d0bb96aeec0a77310c70875343737ae72cffa7044"; when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER)).thenReturn(signature); + when(req.getHeader(RequirePostWithGHHookPayload.Processor.SIGNATURE_HEADER_SHA256)).thenReturn(signature256); doReturn(PAYLOAD).when(processor).payloadFrom(req, null); processor.shouldProvideValidSignature(req, null); @@ -143,7 +152,7 @@ public void shouldPassWithValidSignature() throws Exception { @Test @Issue("JENKINS-37481") - public void shouldIgnoreSignHeaderOnNotDefinedSignInConfig() throws Exception { + void shouldIgnoreSignHeaderOnNotDefinedSignInConfig() throws Exception { removeSecret(); final String signature = "sha1=49d5f5cf800a81f257324912969a2d325d13d3fc"; @@ -153,7 +162,7 @@ public void shouldIgnoreSignHeaderOnNotDefinedSignInConfig() throws Exception { } @Test - public void shouldReturnValidPayloadOnApplicationJson() { + void shouldReturnValidPayloadOnApplicationJson() { final String payload = "test"; doReturn(GHEventPayload.PayloadHandler.APPLICATION_JSON).when(req).getContentType(); @@ -164,7 +173,7 @@ public void shouldReturnValidPayloadOnApplicationJson() { } @Test - public void shouldReturnValidPayloadOnFormUrlEncoded() { + void shouldReturnValidPayloadOnFormUrlEncoded() { final String payload = "test"; doReturn(GHEventPayload.PayloadHandler.FORM_URLENCODED).when(req).getContentType(); diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithmTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithmTest.java new file mode 100644 index 000000000..03b527923 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/SignatureAlgorithmTest.java @@ -0,0 +1,40 @@ +package org.jenkinsci.plugins.github.webhook; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * Tests for {@link SignatureAlgorithm}. + * + * @since 1.45.0 + */ +class SignatureAlgorithmTest { + + @Test + void shouldHaveCorrectSHA256Properties() { + SignatureAlgorithm algorithm = SignatureAlgorithm.SHA256; + + assertThat("SHA-256 prefix", algorithm.getPrefix(), equalTo("sha256")); + assertThat("SHA-256 header", algorithm.getHeaderName(), equalTo("X-Hub-Signature-256")); + assertThat("SHA-256 Java algorithm", algorithm.getJavaAlgorithm(), equalTo("HmacSHA256")); + assertThat("SHA-256 signature prefix", algorithm.getSignaturePrefix(), equalTo("sha256=")); + } + + @Test + void shouldHaveCorrectSHA1Properties() { + SignatureAlgorithm algorithm = SignatureAlgorithm.SHA1; + + assertThat("SHA-1 prefix", algorithm.getPrefix(), equalTo("sha1")); + assertThat("SHA-1 header", algorithm.getHeaderName(), equalTo("X-Hub-Signature")); + assertThat("SHA-1 Java algorithm", algorithm.getJavaAlgorithm(), equalTo("HmacSHA1")); + assertThat("SHA-1 signature prefix", algorithm.getSignaturePrefix(), equalTo("sha1=")); + } + + @Test + void shouldDefaultToSHA256() { + assertThat("Default algorithm should be SHA-256", + SignatureAlgorithm.getDefault(), equalTo(SignatureAlgorithm.SHA256)); + } +} \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java index f6217fe1a..3f68c066f 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/WebhookManagerTest.java @@ -11,18 +11,21 @@ import hudson.plugins.git.GitSCM; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GHEvent; import org.kohsuke.github.GHHook; import org.kohsuke.github.GHRepository; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; import java.net.MalformedURLException; @@ -34,22 +37,22 @@ import static com.google.common.collect.ImmutableList.copyOf; import static com.google.common.collect.Lists.asList; import static com.google.common.collect.Lists.newArrayList; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.jenkinsci.plugins.github.test.HookSecretHelper.storeSecretIn; import static org.jenkinsci.plugins.github.webhook.WebhookManager.forHookUrl; -import static org.junit.Assert.assertThat; import static org.kohsuke.github.GHEvent.CREATE; import static org.kohsuke.github.GHEvent.PULL_REQUEST; import static org.kohsuke.github.GHEvent.PUSH; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyListOf; -import static org.mockito.Matchers.anySetOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.argThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -60,15 +63,15 @@ /** * @author lanwen (Merkushev Kirill) */ -@RunWith(MockitoJUnitRunner.class) +@WithJenkins +@ExtendWith(MockitoExtension.class) public class WebhookManagerTest { public static final GitSCM GIT_SCM = new GitSCM("ssh://git@github.com/dummy/dummy.git"); public static final URL HOOK_ENDPOINT = endpoint("http://hook.endpoint/"); public static final URL ANOTHER_HOOK_ENDPOINT = endpoint("http://another.url/"); - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; @Spy private WebhookManager manager = forHookUrl(HOOK_ENDPOINT); @@ -82,16 +85,23 @@ public class WebhookManagerTest { @Mock private GHRepository repo; + @Captor + ArgumentCaptor> captor; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; + } @Test - public void shouldDoNothingOnNoAdminRights() throws Exception { + void shouldDoNothingOnNoAdminRights() throws Exception { manager.unregisterFor(nonactive, newArrayList(active)); verify(manager, never()).withAdminAccess(); verify(manager, never()).fetchHooks(); } @Test - public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception { + void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception { doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); @@ -103,7 +113,7 @@ public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception } @Test - public void shouldSearchOnlyServiceHookOnActiveName() throws Exception { + void shouldSearchOnlyServiceHookOnActiveName() throws Exception { doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); @@ -116,7 +126,7 @@ public void shouldSearchOnlyServiceHookOnActiveName() throws Exception { @Test @WithoutJenkins - public void shouldMatchAdminAccessWhenTrue() throws Exception { + void shouldMatchAdminAccessWhenTrue() throws Exception { when(repo.hasAdminAccess()).thenReturn(true); assertThat("has admin access", manager.withAdminAccess().apply(repo), is(true)); @@ -124,7 +134,7 @@ public void shouldMatchAdminAccessWhenTrue() throws Exception { @Test @WithoutJenkins - public void shouldMatchAdminAccessWhenFalse() throws Exception { + void shouldMatchAdminAccessWhenFalse() throws Exception { when(repo.hasAdminAccess()).thenReturn(false); assertThat("has no admin access", manager.withAdminAccess().apply(repo), is(false)); @@ -132,8 +142,8 @@ public void shouldMatchAdminAccessWhenFalse() throws Exception { @Test @WithoutJenkins - public void shouldMatchWebHook() { - when(repo.hasAdminAccess()).thenReturn(false); + void shouldMatchWebHook() { + lenient().when(repo.hasAdminAccess()).thenReturn(false); GHHook hook = hook(HOOK_ENDPOINT, PUSH); @@ -142,8 +152,8 @@ public void shouldMatchWebHook() { @Test @WithoutJenkins - public void shouldNotMatchOtherUrlWebHook() { - when(repo.hasAdminAccess()).thenReturn(false); + void shouldNotMatchOtherUrlWebHook() { + lenient().when(repo.hasAdminAccess()).thenReturn(false); GHHook hook = hook(ANOTHER_HOOK_ENDPOINT, PUSH); @@ -152,7 +162,7 @@ public void shouldNotMatchOtherUrlWebHook() { } @Test - public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException { + void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException { doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); Predicate del = spy(Predicate.class); @@ -168,7 +178,7 @@ public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException } @Test - public void shouldNotReplaceAlreadyRegisteredHook() throws IOException { + void shouldNotReplaceAlreadyRegisteredHook() throws IOException { doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); @@ -177,12 +187,12 @@ public void shouldNotReplaceAlreadyRegisteredHook() throws IOException { manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive); verify(manager, never()).deleteWebhook(); - verify(manager, never()).createWebhook(any(URL.class), anySetOf(GHEvent.class)); + verify(manager, never()).createWebhook(any(URL.class), anySet()); } @Test - @Issue( "JENKINS-62116" ) - public void shouldNotReplaceAlreadyRegisteredHookWithMoreEvents() throws IOException { + @Issue("JENKINS-62116") + void shouldNotReplaceAlreadyRegisteredHookWithMoreEvents() throws IOException { doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); @@ -191,21 +201,21 @@ public void shouldNotReplaceAlreadyRegisteredHookWithMoreEvents() throws IOExcep manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive); verify(manager, never()).deleteWebhook(); - verify(manager, never()).createWebhook(any(URL.class), anySetOf(GHEvent.class)); + verify(manager, never()).createWebhook(any(URL.class), anySet()); } @Test - public void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOException { + void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOException { FreeStyleProject project = jenkins.createFreeStyleProject(); project.setScm(GIT_SCM); manager.registerFor((Item)project).run(); - verify(manager, never()).createHookSubscribedTo(anyListOf(GHEvent.class)); + verify(manager, never()).createHookSubscribedTo(anyList()); } @Test - public void shouldAddPushEventByDefault() throws IOException { + void shouldAddPushEventByDefault() throws IOException { FreeStyleProject project = jenkins.createFreeStyleProject(); project.addTrigger(new GitHubPushTrigger()); project.setScm(GIT_SCM); @@ -215,7 +225,7 @@ public void shouldAddPushEventByDefault() throws IOException { } @Test - public void shouldReturnNullOnGettingEmptyEventsListToSubscribe() throws IOException { + void shouldReturnNullOnGettingEmptyEventsListToSubscribe() throws IOException { doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class)); when(repo.hasAdminAccess()).thenReturn(true); @@ -225,7 +235,7 @@ public void shouldReturnNullOnGettingEmptyEventsListToSubscribe() throws IOExcep } @Test - public void shouldSelectOnlyHookManagedCreds() { + void shouldSelectOnlyHookManagedCreds() { GitHubServerConfig conf = new GitHubServerConfig(""); conf.setManageHooks(false); GitHubPlugin.configuration().getConfigs().add(conf); @@ -235,7 +245,7 @@ public void shouldSelectOnlyHookManagedCreds() { } @Test - public void shouldNotSelectCredsWithCustomHost() { + void shouldNotSelectCredsWithCustomHost() { GitHubServerConfig conf = new GitHubServerConfig(""); conf.setApiUrl(ANOTHER_HOOK_ENDPOINT.toString()); conf.setManageHooks(false); @@ -246,7 +256,7 @@ public void shouldNotSelectCredsWithCustomHost() { } @Test - public void shouldSendSecretIfDefined() throws Exception { + void shouldSendSecretIfDefined() throws Exception { String secretText = "secret_text"; storeSecretIn(GitHubPlugin.configuration(), secretText); @@ -255,10 +265,11 @@ public void shouldSendSecretIfDefined() throws Exception { verify(repo).createHook( anyString(), - (Map) argThat(hasEntry("secret", secretText)), - anySetOf(GHEvent.class), + captor.capture(), + anySet(), anyBoolean() ); + assertThat(captor.getValue(), hasEntry("secret", secretText)); } @@ -266,7 +277,7 @@ private GHHook hook(URL endpoint, GHEvent event, GHEvent... events) { GHHook hook = mock(GHHook.class); when(hook.getName()).thenReturn("web"); when(hook.getConfig()).thenReturn(ImmutableMap.of("url", endpoint.toExternalForm())); - when(hook.getEvents()).thenReturn(EnumSet.copyOf(asList(event, events))); + lenient().when(hook.getEvents()).thenReturn(EnumSet.copyOf(asList(event, events))); return hook; } diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java index 78851d578..0a20c01a5 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/DefaultPushGHEventListenerTest.java @@ -1,62 +1,97 @@ package org.jenkinsci.plugins.github.webhook.subscriber; import com.cloudbees.jenkins.GitHubPushTrigger; +import com.cloudbees.jenkins.GitHubRepositoryNameContributor; import com.cloudbees.jenkins.GitHubTriggerEvent; +import hudson.ExtensionList; import hudson.model.FreeStyleProject; +import hudson.model.Item; import hudson.plugins.git.GitSCM; +import jenkins.model.Jenkins; import org.jenkinsci.plugins.github.extension.GHSubscriberEvent; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.WithoutJenkins; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GHEvent; +import org.mockito.MockedStatic; import org.mockito.Mockito; +import java.util.Collections; +import java.util.List; + import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author lanwen (Merkushev Kirill) */ +@WithJenkins public class DefaultPushGHEventListenerTest { public static final GitSCM GIT_SCM_FROM_RESOURCE = new GitSCM("ssh://git@github.com/lanwen/test.git"); public static final String TRIGGERED_BY_USER_FROM_RESOURCE = "lanwen"; - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; + } @Test - public void shouldBeNotApplicableForProjectWithoutTrigger() throws Exception { - FreeStyleProject prj = jenkins.createFreeStyleProject(); + @WithoutJenkins + void shouldBeNotApplicableForProjectWithoutTrigger() { + FreeStyleProject prj = mock(FreeStyleProject.class); assertThat(new DefaultPushGHEventSubscriber().isApplicable(prj), is(false)); } @Test - public void shouldBeApplicableForProjectWithTrigger() throws Exception { - FreeStyleProject prj = jenkins.createFreeStyleProject(); - prj.addTrigger(new GitHubPushTrigger()); + @WithoutJenkins + void shouldBeApplicableForProjectWithTrigger() { + FreeStyleProject prj = mock(FreeStyleProject.class); + when(prj.getTriggers()).thenReturn( + Collections.singletonMap(new GitHubPushTrigger.DescriptorImpl(), new GitHubPushTrigger())); assertThat(new DefaultPushGHEventSubscriber().isApplicable(prj), is(true)); } @Test - public void shouldParsePushPayload() throws Exception { + @WithoutJenkins + void shouldParsePushPayload() { GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); - FreeStyleProject prj = jenkins.createFreeStyleProject(); - prj.addTrigger(trigger); - prj.setScm(GIT_SCM_FROM_RESOURCE); + FreeStyleProject prj = mock(FreeStyleProject.class); + when(prj.getTriggers()).thenReturn( + Collections.singletonMap(new GitHubPushTrigger.DescriptorImpl(), trigger)); + when(prj.getSCMs()).thenAnswer(unused -> Collections.singletonList(GIT_SCM_FROM_RESOURCE)); GHSubscriberEvent subscriberEvent = new GHSubscriberEvent("shouldParsePushPayload", GHEvent.PUSH, classpath("payloads/push.json")); - new DefaultPushGHEventSubscriber().onEvent(subscriberEvent); + + Jenkins jenkins = mock(Jenkins.class); + when(jenkins.getAllItems(Item.class)).thenReturn(Collections.singletonList(prj)); + + ExtensionList extensionList = mock(ExtensionList.class); + List gitHubRepositoryNameContributorList = + Collections.singletonList(new GitHubRepositoryNameContributor.FromSCM()); + when(extensionList.iterator()).thenReturn(gitHubRepositoryNameContributorList.iterator()); + when(jenkins.getExtensionList(GitHubRepositoryNameContributor.class)).thenReturn(extensionList); + + try (MockedStatic mockedJenkins = mockStatic(Jenkins.class)) { + mockedJenkins.when(Jenkins::getInstance).thenReturn(jenkins); + new DefaultPushGHEventSubscriber().onEvent(subscriberEvent); + } verify(trigger).onPost(eq(GitHubTriggerEvent.create() .withTimestamp(subscriberEvent.getTimestamp()) @@ -68,7 +103,7 @@ public void shouldParsePushPayload() throws Exception { @Test @Issue("JENKINS-27136") - public void shouldReceivePushHookOnWorkflow() throws Exception { + void shouldReceivePushHookOnWorkflow() throws Exception { WorkflowJob job = jenkins.getInstance().createProject(WorkflowJob.class, "test-workflow-job"); GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); @@ -91,7 +126,7 @@ public void shouldReceivePushHookOnWorkflow() throws Exception { @Test @Issue("JENKINS-27136") - public void shouldNotReceivePushHookOnWorkflowWithNoBuilds() throws Exception { + void shouldNotReceivePushHookOnWorkflowWithNoBuilds() throws Exception { WorkflowJob job = jenkins.getInstance().createProject(WorkflowJob.class, "test-workflow-job"); GitHubPushTrigger trigger = mock(GitHubPushTrigger.class); diff --git a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java index 4d6ae5587..1e29ce021 100644 --- a/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java +++ b/src/test/java/org/jenkinsci/plugins/github/webhook/subscriber/PingGHEventSubscriberTest.java @@ -1,43 +1,49 @@ package org.jenkinsci.plugins.github.webhook.subscriber; import hudson.model.FreeStyleProject; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.WithoutJenkins; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; import org.kohsuke.github.GHEvent; import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import org.jvnet.hudson.test.Issue; /** * @author lanwen (Merkushev Kirill) */ -public class PingGHEventSubscriberTest { +@WithJenkins +class PingGHEventSubscriberTest { - @Rule - public JenkinsRule jenkins = new JenkinsRule(); + private JenkinsRule jenkins; + + @BeforeEach + void setUp(JenkinsRule rule) throws Exception { + jenkins = rule; + } @Test - public void shouldBeNotApplicableForProjects() throws Exception { + void shouldBeNotApplicableForProjects() throws Exception { FreeStyleProject prj = jenkins.createFreeStyleProject(); assertThat(new PingGHEventSubscriber().isApplicable(prj), is(false)); } @Test - public void shouldParsePingPayload() throws Exception { + void shouldParsePingPayload() throws Exception { injectedPingSubscr().onEvent(GHEvent.PING, classpath("payloads/ping.json")); } @Issue("JENKINS-30626") @Test @WithoutJenkins - public void shouldParseOrgPingPayload() throws Exception { + void shouldParseOrgPingPayload() throws Exception { new PingGHEventSubscriber().onEvent(GHEvent.PING, classpath("payloads/orgping.json")); } - + private PingGHEventSubscriber injectedPingSubscr() { PingGHEventSubscriber pingSubsc = new PingGHEventSubscriber(); jenkins.getInstance().getInjector().injectMembers(pingSubsc); diff --git a/src/test/resources/checkstyle/checkstyle-config.xml b/src/test/resources/checkstyle/checkstyle-config.xml index 36c586e1f..0d7b59d55 100644 --- a/src/test/resources/checkstyle/checkstyle-config.xml +++ b/src/test/resources/checkstyle/checkstyle-config.xml @@ -43,9 +43,7 @@ - - - + diff --git a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json index 0d006823d..203839f23 100644 --- a/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json +++ b/src/test/resources/com/cloudbees/jenkins/GitHubWebHookFullTest/payloads/push.json @@ -65,7 +65,7 @@ "html_url": "https://github.com/lanwen/test", "description": "Personal blog", "fork": false, - "url": "https://github.com/lanwen/test", + "url": "https://api.github.com/lanwen/test", "forks_url": "https://api.github.com/repos/lanwen/test/forks", "keys_url": "https://api.github.com/repos/lanwen/test/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/lanwen/test/collaborators{/collaborator}", diff --git a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml index b11975415..d55e17eca 100644 --- a/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml +++ b/src/test/resources/org/jenkinsci/plugins/github/migration/MigratorTest/shouldLoadDataAfterStart/config.xml @@ -1,7 +1,7 @@ - 1.554.1 + 1.565.11 2 NORMAL true diff --git a/src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/repos-repo.json b/src/test/resources/org/jenkinsci/plugins/github/test/GitHubMockExtension/repos-repo.json similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/repos-repo.json rename to src/test/resources/org/jenkinsci/plugins/github/test/GitHubMockExtension/repos-repo.json diff --git a/src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/user.json b/src/test/resources/org/jenkinsci/plugins/github/test/GitHubMockExtension/user.json similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/github/test/GHMockRule/user.json rename to src/test/resources/org/jenkinsci/plugins/github/test/GitHubMockExtension/user.json