diff --git a/.env b/.env index a8e16ee5d..b7673033d 100644 --- a/.env +++ b/.env @@ -4,5 +4,5 @@ SONARQUBE_VERSION=latest # The name of the Dockerfile to run. 'Dockerfile' is building locally, 'release.Dockerfile' if building the release image DOCKERFILE=Dockerfile -# The version of the plugin to include in the image; only relevant if 'release.Dockerfile' is being used -PLUGIN_VERSION=1.7.0 +# The version of the plugin to include in the image +PLUGIN_VERSION=1.8.0-SNAPSHOT diff --git a/Dockerfile b/Dockerfile index 4466e827e..9c3de7a6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,9 @@ WORKDIR /home/build/project RUN ./gradlew build -x test FROM sonarqube:${SONARQUBE_VERSION} -COPY --from=builder --chown=sonarqube:sonarqube /home/build/project/build/libs/sonarqube-community-branch-plugin-*.jar /opt/sonarqube/lib/common/ COPY --from=builder --chown=sonarqube:sonarqube /home/build/project/build/libs/sonarqube-community-branch-plugin-*.jar /opt/sonarqube/extensions/plugins/ + +ARG PLUGIN_VERSION +ENV PLUGIN_VERSION=${PLUGIN_VERSION} +ENV SONAR_WEB_JAVAADDITIONALOPTS="-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${PLUGIN_VERSION}.jar=web" +ENV SONAR_CE_JAVAADDITIONALOPTS="-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${PLUGIN_VERSION}.jar=ce" diff --git a/README.md b/README.md index 4f27305a4..9baf73f5f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,21 @@ SonarQube Version | Plugin Version The plugin is intended to support the [features and parameters specified in the SonarQube documentation](https://docs.sonarqube.org/latest/branches/overview/). # Installation -Either build the project or [download a compatible release version of the plugin JAR](https://github.com/mc1arke/sonarqube-community-branch-plugin/releases). Copy the plugin JAR file to the `extensions/plugins/` **and** the `lib/common/` directories of your SonarQube instance and restart SonarQube. + +## Manual Install +__Please ensure you follow the installation instructions for the version of the plugin you're installing by looking at the README on the relevant release tag.__ + +Either build the project or [download a compatible release version of the plugin JAR](https://github.com/mc1arke/sonarqube-community-branch-plugin/releases). + +1. Copy the plugin JAR file to the `extensions/plugins/` directory of your SonarQube instance +2. Add `-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${version}.jar=web` to the `sonar.web.javaAdditionalOptions` property in your Sonarqube installation's `config/sonar.properties` file, e.g. `sonar.web.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.0.jar=web` +3. Add `-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${version}.jar=ce` to the `sonar.ce.javaAdditionalOptions` property in your Sonarqube installation's `config/sonar.properties` file, e.g. `sonar.ce.javaAdditionalOpts=-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-1.8.0.jar=ce` +4. Start Sonarqube, and accept the warning about using third-party plugins + +## Docker +The plugin is distributed in the [mc1arke/sonarqube-with-community-branch-plugin](https://hub.docker.com/r/mc1arke/sonarqube-with-community-branch-plugin) Docker image, with the image versions matching the up-stream Sonarqube image version. + +__Note:__ If you're setting the `SONAR_WEB_JAVAADDITIONALOPTS` or `SONAR_CE_JAVAADDITIONALOPTS` environment variables in your container launch then you'll need to add the `javaagent` configuration to your overrides to match what's in the provided Dockerfile. # Configuration ## Global configuration diff --git a/build.gradle b/build.gradle index f06edc5f0..54b2a5155 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ repositories { } } -def sonarqubeVersion = '8.7.0.41497' +def sonarqubeVersion = '8.9.0.43852' def sonarqubeLibDir = "${projectDir}/sonarqube-lib" def sonarLibraries = "${sonarqubeLibDir}/sonarqube-${sonarqubeVersion}/lib" @@ -64,6 +64,7 @@ dependencies { compile files('lib/nodes-0.5.0.jar') runtime 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.7' compileOnly 'com.google.code.findbugs:jsr305:3.0.2' + compile 'org.javassist:javassist:3.27.0-GA' } @@ -92,7 +93,9 @@ jar { 'Plugin-IssueTrackerUrl': 'https://github.com/mc1arke/sonarqube-community-branch-plugin/issues', 'Plugin-Key': 'communityBranchPlugin', 'Plugin-Class': 'com.github.mc1arke.sonarqube.plugin.CommunityBranchPluginBootstrap', - 'Plugin-Name': 'Community Branch Plugin' + 'Plugin-Name': 'Community Branch Plugin', + 'Premain-Class': 'com.github.mc1arke.sonarqube.plugin.CommunityBranchAgent', + 'Can-Retransform-Classes': 'true' } } @@ -132,4 +135,4 @@ jacocoTestReport { plugins.withType(JacocoPlugin) { tasks["test"].finalizedBy 'jacocoTestReport' -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index 8070b31cc..22e57a524 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,9 +17,9 @@ services: networks: - sonarnet environment: - - SONARQUBE_JDBC_URL=jdbc:postgresql://db:5432/sonar - - SONARQUBE_JDBC_USERNAME=sonar - - SONARQUBE_JDBC_PASSWORD=sonar + - SONAR_JDBC_URL=jdbc:postgresql://db:5432/sonar + - SONAR_JDBC_USERNAME=sonar + - SONAR_JDBC_PASSWORD=sonar volumes: - sonarqube_conf:/opt/sonarqube/conf - sonarqube_data:/opt/sonarqube/data diff --git a/release.Dockerfile b/release.Dockerfile index 46e272b8c..b82d214c3 100644 --- a/release.Dockerfile +++ b/release.Dockerfile @@ -5,5 +5,7 @@ FROM sonarqube:${SONARQUBE_VERSION} ARG PLUGIN_VERSION ENV PLUGIN_VERSION=${PLUGIN_VERSION} -ADD --chown=sonarqube:sonarqube https://github.com/mc1arke/sonarqube-community-branch-plugin/releases/download/${PLUGIN_VERSION}/sonarqube-community-branch-plugin-${PLUGIN_VERSION}.jar /opt/sonarqube/lib/common/ ADD --chown=sonarqube:sonarqube https://github.com/mc1arke/sonarqube-community-branch-plugin/releases/download/${PLUGIN_VERSION}/sonarqube-community-branch-plugin-${PLUGIN_VERSION}.jar /opt/sonarqube/extensions/plugins/ + +ENV SONAR_WEB_JAVAADDITIONALOPTS="-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${PLUGIN_VERSION}.jar=web" +ENV SONAR_CE_JAVAADDITIONALOPTS="-javaagent:./extensions/plugins/sonarqube-community-branch-plugin-${PLUGIN_VERSION}.jar=ce" diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchAgent.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchAgent.java new file mode 100644 index 000000000..b94f76e30 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchAgent.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin; + +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; +import javassist.NotFoundException; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import java.io.IOException; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.security.ProtectionDomain; + +public final class CommunityBranchAgent { + + private static final Logger LOGGER = Loggers.get(CommunityBranchAgent.class); + + private CommunityBranchAgent() { + super(); + } + + public static void premain(String args, Instrumentation instrumentation) throws UnmodifiableClassException, ClassNotFoundException { + LOGGER.info("Loading agent"); + + if (!"ce".equals(args) && !"web".equals(args)) { + throw new IllegalArgumentException("Invalid/missing agent argument"); + } + + if ("ce".equals(args)) { + redefineEdition(instrumentation); + } + } + + private static void redefineEdition(Instrumentation instrumentation) throws ClassNotFoundException, UnmodifiableClassException { + String targetClassName = "org.sonar.core.platform.PlatformEditionProvider"; + + instrumentation.addTransformer(new ClassFileTransformer() { + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] byteCode) { + + String finalTargetClassName = targetClassName.replace(".", "/"); + + if (!className.equals(finalTargetClassName)) { + return byteCode; + } + + LOGGER.debug("Transforming class " + targetClassName); + try { + ClassPool cp = ClassPool.getDefault(); + CtClass cc = cp.get(targetClassName); + CtMethod m = cc.getDeclaredMethod("get"); + m.setBody("return java.util.Optional.of(org.sonar.core.platform.EditionProvider.Edition.DEVELOPER);"); + + byteCode = cc.toBytecode(); + cc.detach(); + } catch (NotFoundException | CannotCompileException | IOException e) { + LOGGER.error("Could not transform class, will use default class definition", e); + } + + return byteCode; + } + + }); + + instrumentation.retransformClasses(Class.forName(targetClassName)); + } + +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java index 3f75b429e..7091869b4 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java @@ -18,7 +18,6 @@ */ package com.github.mc1arke.sonarqube.plugin; -import com.github.mc1arke.sonarqube.plugin.ce.CommunityBranchEditionProvider; import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchParamsValidator; @@ -27,14 +26,13 @@ import com.github.mc1arke.sonarqube.plugin.scanner.ScannerPullRequestPropertySensor; import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchFeatureExtension; import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchSupportDelegate; -import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.CountBindingAction; +import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.CreateBitbucketCloudAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.DeleteBindingAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.SetAzureBindingAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.SetBitbucketBindingAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.SetBitbucketCloudBindingAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.SetGithubBindingAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.SetGitlabBindingAction; -import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.CreateBitbucketCloudAction; import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action.UpdateBitbucketCloudAction; import org.sonar.api.CoreProperties; import org.sonar.api.Plugin; @@ -60,11 +58,9 @@ public String getName() { @Override public void load(CoreExtension.Context context) { if (SonarQubeSide.COMPUTE_ENGINE == context.getRuntime().getSonarQubeSide()) { - context.addExtensions(CommunityReportAnalysisComponentProvider.class, CommunityBranchEditionProvider.class); + context.addExtensions(CommunityReportAnalysisComponentProvider.class); } else if (SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class, - - CountBindingAction.class, DeleteBindingAction.class, SetGithubBindingAction.class, SetAzureBindingAction.class, diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchEditionProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchEditionProvider.java deleted file mode 100644 index c6f5d92b2..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchEditionProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - */ -package com.github.mc1arke.sonarqube.plugin.ce; - -import org.sonar.core.platform.EditionProvider; - -import java.util.Optional; - -public class CommunityBranchEditionProvider implements EditionProvider { - - @Override - public Optional get() { - return Optional.of(Edition.DEVELOPER); - } - -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java deleted file mode 100644 index d11a69692..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingAction.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2020-2021 Michael Clarke - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - */ -package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; - -import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; -import org.sonar.api.server.ws.WebService; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.server.user.UserSession; -import org.sonar.server.ws.WsUtils; -import org.sonarqube.ws.AlmSettings.CountBindingWsResponse; - - -public class CountBindingAction extends AlmSettingsWebserviceAction { - - private static final String ALM_SETTING_PARAMETER = "almSetting"; - - private final DbClient dbClient; - private final UserSession userSession; - private final ProtoBufWriter protoBufWriter; - - public CountBindingAction(DbClient dbClient, UserSession userSession) { - this(dbClient, userSession, WsUtils::writeProtobuf); - } - - CountBindingAction(DbClient dbClient, UserSession userSession, ProtoBufWriter protoBufWriter) { - super(dbClient); - this.dbClient = dbClient; - this.userSession = userSession; - this.protoBufWriter = protoBufWriter; - } - - @Override - public void define(WebService.NewController context) { - WebService.NewAction action = context.createAction("count_binding").setHandler(this); - - action.createParam(ALM_SETTING_PARAMETER).setRequired(true); - } - - @Override - public void handle(Request request, Response response) { - userSession.checkIsSystemAdministrator(); - - String almSettingKey = request.mandatoryParam(ALM_SETTING_PARAMETER); - try (DbSession dbSession = dbClient.openSession(false)) { - AlmSettingDto almSetting = getAlmSetting(dbSession, almSettingKey); - int projectCount = dbClient.projectAlmSettingDao().countByAlmSetting(dbSession, almSetting); - CountBindingWsResponse.Builder builder = - CountBindingWsResponse.newBuilder().setKey(almSetting.getKey()).setProjects(projectCount); - - protoBufWriter.write(builder.build(), request, response); - } - - } - -} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java deleted file mode 100644 index 744698f1d..000000000 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingAction.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2020 Michael Clarke - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - */ -package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; - -import com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.AlmTypeMapper; -import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; -import org.sonar.api.server.ws.WebService; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; -import org.sonar.db.project.ProjectDto; -import org.sonar.server.component.ComponentFinder; -import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.user.UserSession; -import org.sonar.server.ws.WsUtils; -import org.sonarqube.ws.AlmSettings.GetBindingWsResponse; - -import java.util.Optional; - -import static java.lang.String.format; - -public class GetBindingAction extends ProjectWsAction { - - private final DbClient dbClient; - private final ProtoBufWriter protoBufWriter; - - public GetBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { - this(dbClient, componentFinder, userSession, WsUtils::writeProtobuf); - } - - GetBindingAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, ProtoBufWriter protoBufWriter) { - super("get_binding", dbClient, componentFinder, userSession, true); - this.dbClient = dbClient; - this.protoBufWriter = protoBufWriter; - } - - @Override - protected void configureAction(WebService.NewAction action) { - //no-op - } - - @Override - protected void handleProjectRequest(ProjectDto project, Request request, Response response, DbSession dbSession) { - ProjectAlmSettingDto projectAlmSetting = dbClient.projectAlmSettingDao().selectByProject(dbSession, project) - .orElseThrow(() -> new NotFoundException( - format("Project '%s' is not bound to any ALM", project.getKey()))); - AlmSettingDto almSetting = - dbClient.almSettingDao().selectByUuid(dbSession, projectAlmSetting.getAlmSettingUuid()).orElseThrow( - () -> new IllegalStateException( - format("ALM setting '%s' cannot be found", projectAlmSetting.getAlmSettingUuid()))); - - GetBindingWsResponse.Builder builder = - GetBindingWsResponse.newBuilder().setAlm(AlmTypeMapper.toAlmWs(almSetting.getAlm())) - .setKey(almSetting.getKey()); - Optional.ofNullable(projectAlmSetting.getAlmRepo()).ifPresent(builder::setRepository); - Optional.ofNullable(almSetting.getUrl()).ifPresent(builder::setUrl); - Optional.ofNullable(projectAlmSetting.getAlmSlug()).ifPresent(builder::setSlug); - protoBufWriter.write(builder.build(), request, response); - } - -} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchAgentTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchAgentTest.java new file mode 100644 index 000000000..c47a61945 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchAgentTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021 Michael Clarke + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ +package com.github.mc1arke.sonarqube.plugin; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.sonar.core.platform.EditionProvider; +import org.sonar.core.platform.PlatformEditionProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class CommunityBranchAgentTest { + + @Test + public void checkErrorThrownIfAgentArgsNotValid() { + assertThatThrownBy(() -> CommunityBranchAgent.premain("badarg", mock(Instrumentation.class))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid/missing agent argument"); + } + + + @Test + public void checkNoRedefineForWebLaunch() throws UnmodifiableClassException, ClassNotFoundException { + Instrumentation instrumentation = mock(Instrumentation.class); + + CommunityBranchAgent.premain("web", instrumentation); + + verifyNoInteractions(instrumentation); + } + + @Test + public void checkRedefineForCeLaunchSkipsNonTargetClass() throws UnmodifiableClassException, ClassNotFoundException, IllegalClassFormatException { + Instrumentation instrumentation = mock(Instrumentation.class); + + CommunityBranchAgent.premain("ce", instrumentation); + + ArgumentCaptor classFileTransformerArgumentCaptor = ArgumentCaptor.forClass(ClassFileTransformer.class); + verify(instrumentation).retransformClasses(PlatformEditionProvider.class); + verify(instrumentation).addTransformer(classFileTransformerArgumentCaptor.capture()); + + byte[] input = new byte[]{1, 2, 3, 4, 5, 6}; + byte[] result = classFileTransformerArgumentCaptor.getValue().transform(getClass().getClassLoader(), "com/github/mc1arke/Dummy", getClass(), getClass().getProtectionDomain(), input); + + assertThat(result).isEqualTo(input); + } + + + @Test + public void checkRedefineForCeLaunchRedefinesTargetClass() throws ReflectiveOperationException, IOException, UnmodifiableClassException, IllegalClassFormatException { + Instrumentation instrumentation = mock(Instrumentation.class); + + CommunityBranchAgent.premain("ce", instrumentation); + + ArgumentCaptor classFileTransformerArgumentCaptor = ArgumentCaptor.forClass(ClassFileTransformer.class); + verify(instrumentation).retransformClasses(PlatformEditionProvider.class); + verify(instrumentation).addTransformer(classFileTransformerArgumentCaptor.capture()); + + try (InputStream inputStream = PlatformEditionProvider.class.getResourceAsStream(PlatformEditionProvider.class.getSimpleName())) { + byte[] input = IOUtils.toByteArray(inputStream); + byte[] result = classFileTransformerArgumentCaptor.getValue().transform(getClass().getClassLoader(), PlatformEditionProvider.class.getName().replaceAll("\\.", "/"), getClass(), getClass().getProtectionDomain(), input); + + CustomClassloader classLoader = new CustomClassloader(); + + Class redefined = (Class) classLoader.loadClass(PlatformEditionProvider.class.getName(), result); + assertThat(redefined.getConstructor().newInstance().get()).isEqualTo(Optional.of(EditionProvider.Edition.DEVELOPER)); + } + } + + private static class CustomClassloader extends ClassLoader { + + public Class loadClass(String name, byte[] value) { + return defineClass(name, value, 0, value.length); + } + + }; + +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java index 6e965c12a..0d20544e3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java @@ -18,7 +18,6 @@ */ package com.github.mc1arke.sonarqube.plugin; -import com.github.mc1arke.sonarqube.plugin.ce.CommunityBranchEditionProvider; import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchParamsValidator; @@ -36,6 +35,7 @@ import org.sonar.core.extension.CoreExtension; import java.util.Arrays; +import java.util.Collections; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -102,8 +102,8 @@ public void testComputeEngineSideLoad() { verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); - assertEquals(Arrays.asList(CommunityReportAnalysisComponentProvider.class, CommunityBranchEditionProvider.class), - argumentCaptor.getAllValues().subList(0, 2)); + assertEquals(Collections.singletonList(CommunityReportAnalysisComponentProvider.class), + argumentCaptor.getAllValues().subList(0, 1)); } @@ -119,7 +119,7 @@ public void testServerSideLoad() { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); - assertEquals(13, argumentCaptor.getAllValues().size()); + assertEquals(12, argumentCaptor.getAllValues().size()); assertEquals(Arrays.asList(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class), argumentCaptor.getAllValues().subList(0, 2)); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchEditionProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchEditionProviderTest.java deleted file mode 100644 index a0e37a1c3..000000000 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchEditionProviderTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2019 Michael Clarke - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - */ -package com.github.mc1arke.sonarqube.plugin.ce; - -import org.junit.Assert; -import org.junit.Test; -import org.sonar.core.platform.EditionProvider; - -import java.util.Optional; - -public class CommunityBranchEditionProviderTest { - - @Test - public void testGetEdition() { - Assert.assertEquals(Optional.of(EditionProvider.Edition.DEVELOPER), new CommunityBranchEditionProvider().get()); - } - -} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingActionTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingActionTest.java deleted file mode 100644 index 9c23051d1..000000000 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/CountBindingActionTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Optional; - -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; -import org.sonar.api.server.ws.WebService; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.alm.setting.ALM; -import org.sonar.db.alm.setting.AlmSettingDao; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDao; -import org.sonar.server.user.UserSession; -import org.sonarqube.ws.AlmSettings; - -import com.google.protobuf.Message; - -public class CountBindingActionTest { - - @Test - public void testDefine() { - DbClient dbClient = mock(DbClient.class); - UserSession userSession = mock(UserSession.class); - CountBindingAction testCase = new CountBindingAction(dbClient, userSession); - - WebService.NewParam param = mock(WebService.NewParam.class); - WebService.NewController newController = mock(WebService.NewController.class); - WebService.NewAction newAction = mock(WebService.NewAction.class); - when(newController.createAction(eq("count_binding"))).thenReturn(newAction); - when(newAction.setHandler(eq(testCase))).thenReturn(newAction); - when(newAction.createParam(eq("almSetting"))).thenReturn(param); - - testCase.define(newController); - - verify(newAction).setHandler(eq(testCase)); - verify(param).setRequired(true); - } - - @Test - public void testHandle() { - DbClient dbClient = mock(DbClient.class); - DbSession dbSession = mock(DbSession.class); - when(dbClient.openSession(eq(false))).thenReturn(dbSession); - AlmSettingDao almSettingDao = mock(AlmSettingDao.class); - - AlmSettingDto githubAlmSettingDto = mock(AlmSettingDto.class); - when(githubAlmSettingDto.getAlm()).thenReturn(ALM.GITHUB); - when(githubAlmSettingDto.getKey()).thenReturn("githubKey"); - when(githubAlmSettingDto.getUrl()).thenReturn("githubUrl"); - when(githubAlmSettingDto.getAppId()).thenReturn("githubAppId"); - when(githubAlmSettingDto.getPrivateKey()).thenReturn("githubPrivateKey"); - - when(almSettingDao.selectByKey(eq(dbSession), eq("almSetting"))).thenReturn(Optional.of(githubAlmSettingDto)); - when(dbClient.almSettingDao()).thenReturn(almSettingDao); - - ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); - when(projectAlmSettingDao.countByAlmSetting(eq(dbSession), eq(githubAlmSettingDto))).thenReturn(1234); - UserSession userSession = mock(UserSession.class); - when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - - ProtoBufWriter protoBufWriter = mock(ProtoBufWriter.class); - - CountBindingAction testCase = new CountBindingAction(dbClient, userSession, protoBufWriter); - - Request request = mock(Request.class, Mockito.RETURNS_DEEP_STUBS); - Response response = mock(Response.class, Mockito.RETURNS_DEEP_STUBS); - - when(request.mandatoryParam("almSetting")).thenReturn("almSetting"); - - testCase.handle(request, response); - - ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); - verify(protoBufWriter).write(messageArgumentCaptor.capture(), eq(request), eq(response)); - Message message = messageArgumentCaptor.getValue(); - - AlmSettings.CountBindingWsResponse expectedResponse = AlmSettings.CountBindingWsResponse.newBuilder() - .setKey("githubKey") - .setProjects(1234) - .build(); - - assertThat(message).isInstanceOf(AlmSettings.CountBindingWsResponse.class).isEqualTo(expectedResponse); - } - -} \ No newline at end of file diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingActionTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingActionTest.java deleted file mode 100644 index 4f2a260de..000000000 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/server/pullrequest/ws/action/GetBindingActionTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.github.mc1arke.sonarqube.plugin.server.pullrequest.ws.action; - -import com.google.protobuf.Message; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; -import org.sonar.api.server.ws.WebService; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.alm.setting.ALM; -import org.sonar.db.alm.setting.AlmSettingDao; -import org.sonar.db.alm.setting.AlmSettingDto; -import org.sonar.db.alm.setting.ProjectAlmSettingDao; -import org.sonar.db.alm.setting.ProjectAlmSettingDto; -import org.sonar.db.project.ProjectDto; -import org.sonar.server.component.ComponentFinder; -import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.user.UserSession; -import org.sonarqube.ws.AlmSettings; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class GetBindingActionTest { - - @Test - public void testDefine() { - DbClient dbClient = mock(DbClient.class); - ComponentFinder componentFinder = mock(ComponentFinder.class); - UserSession userSession = mock(UserSession.class); - GetBindingAction testCase = new GetBindingAction(dbClient, componentFinder, userSession); - Map paramMap = new HashMap<>(); - - WebService.NewController newController = mock(WebService.NewController.class); - WebService.NewAction newAction = mock(WebService.NewAction.class); - when(newController.createAction(any())).thenReturn(newAction); - when(newAction.setPost(eq(true))).thenReturn(newAction); - when(newAction.setHandler(eq(testCase))).thenReturn(newAction); - when(newAction.createParam(any())).then(i -> { - WebService.NewParam newParam = mock(WebService.NewParam.class); - paramMap.put(i.getArgument(0), newParam); - return newParam; - }); - testCase.define(newController); - - verify(newAction).setHandler(eq(testCase)); - verify(newAction).createParam(eq("project")); - verify(paramMap.get("project")).setRequired(true); - } - - @Test - public void testHandle() { - DbClient dbClient = mock(DbClient.class); - DbSession dbSession = mock(DbSession.class); - when(dbClient.openSession(eq(false))).thenReturn(dbSession); - AlmSettingDao almSettingDao = mock(AlmSettingDao.class); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUuid()).thenReturn("almSettingsUuid"); - when(almSettingDao.selectByUuid(eq(dbSession), eq("almSettingUuid"))).thenReturn(Optional.of(almSettingDto)); - when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); - when(almSettingDto.getKey()).thenReturn("key"); - when(almSettingDto.getUrl()).thenReturn("url"); - when(dbClient.almSettingDao()).thenReturn(almSettingDao); - ProjectDto projectDto = mock(ProjectDto.class); - ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); - when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - ComponentFinder componentFinder = mock(ComponentFinder.class); - when(projectDto.getKey()).thenReturn("projectUuid"); - when(componentFinder.getProjectByKey(eq(dbSession), eq("project"))).thenReturn(projectDto); - UserSession userSession = mock(UserSession.class); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almSettingUuid"); - when(projectAlmSettingDto.getAlmRepo()).thenReturn("repository"); - when(projectAlmSettingDto.getAlmSlug()).thenReturn("slug"); - when(projectAlmSettingDao.selectByProject(eq(dbSession), eq(projectDto))).thenReturn(Optional.of(projectAlmSettingDto)); - ProtoBufWriter protoBufWriter = mock(ProtoBufWriter.class); - - GetBindingAction testCase = new GetBindingAction(dbClient, componentFinder, userSession, protoBufWriter); - - Request request = mock(Request.class, Mockito.RETURNS_DEEP_STUBS); - Response response = mock(Response.class, Mockito.RETURNS_DEEP_STUBS); - - when(request.mandatoryParam("almSetting")).thenReturn("almSetting"); - when(request.param("project")).thenReturn("project"); - when(request.getMediaType()).thenReturn("dummy"); - - testCase.handle(request, response); - - ArgumentCaptor messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); - verify(protoBufWriter).write(messageArgumentCaptor.capture(), eq(request), eq(response)); - Message message = messageArgumentCaptor.getValue(); - - AlmSettings.GetBindingWsResponse expectedResponse = AlmSettings.GetBindingWsResponse.newBuilder() - .setAlm(AlmSettings.Alm.github) - .setRepository("repository") - .setKey("key") - .setUrl("url") - .setSlug("slug") - .build(); - assertThat(message).isInstanceOf(AlmSettings.GetBindingWsResponse.class).isEqualTo(expectedResponse); - } - - @Test - public void testHandleMissingProjectParameter() { - DbClient dbClient = mock(DbClient.class); - DbSession dbSession = mock(DbSession.class); - when(dbClient.openSession(eq(false))).thenReturn(dbSession); - AlmSettingDao almSettingDao = mock(AlmSettingDao.class); - AlmSettingDto almSettingDto = mock(AlmSettingDto.class); - when(almSettingDto.getUuid()).thenReturn("almSettingsUuid"); - when(almSettingDao.selectByUuid(eq(dbSession), eq("almSettingUuid"))).thenReturn(Optional.of(almSettingDto)); - when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB); - when(almSettingDto.getKey()).thenReturn("key"); - when(almSettingDto.getUrl()).thenReturn("url"); - when(dbClient.almSettingDao()).thenReturn(almSettingDao); - ProjectDto projectDto = mock(ProjectDto.class); - ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); - when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - ComponentFinder componentFinder = mock(ComponentFinder.class); - when(projectDto.getKey()).thenReturn("projectUuid"); - when(componentFinder.getProjectByKey(eq(dbSession), eq("project"))).thenReturn(projectDto); - UserSession userSession = mock(UserSession.class); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almSettingUuid"); - when(projectAlmSettingDto.getAlmRepo()).thenReturn("repository"); - when(projectAlmSettingDto.getAlmSlug()).thenReturn("slug"); - when(projectAlmSettingDao.selectByProject(eq(dbSession), eq(projectDto))).thenReturn(Optional.of(projectAlmSettingDto)); - ProtoBufWriter protoBufWriter = mock(ProtoBufWriter.class); - - GetBindingAction testCase = new GetBindingAction(dbClient, componentFinder, userSession, protoBufWriter); - - Request request = mock(Request.class, Mockito.RETURNS_DEEP_STUBS); - Response response = mock(Response.class, Mockito.RETURNS_DEEP_STUBS); - - when(request.mandatoryParam("almSetting")).thenReturn("almSetting"); - when(request.getMediaType()).thenReturn("dummy"); - - assertThatThrownBy(() -> testCase.handle(request, response)) - .isInstanceOf(IllegalArgumentException.class).hasMessage("The 'project' parameter is missing"); - } - - @Test - public void testHandleUnboundProject() { - DbClient dbClient = mock(DbClient.class); - DbSession dbSession = mock(DbSession.class); - when(dbClient.openSession(eq(false))).thenReturn(dbSession); - ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); - - when(projectAlmSettingDao.selectByProject(eq(dbSession), eq("projectUuid"))).thenReturn(Optional.empty()); - when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - - ProjectDto projectDto = mock(ProjectDto.class); - when(projectDto.getKey()).thenReturn("project"); - - ComponentFinder componentFinder = mock(ComponentFinder.class); - when(projectDto.getKey()).thenReturn("project"); - when(componentFinder.getProjectByKey(eq(dbSession), eq("project"))).thenReturn(projectDto); - UserSession userSession = mock(UserSession.class); - - GetBindingAction testCase = new GetBindingAction(dbClient, componentFinder, userSession); - - Request request = mock(Request.class, Mockito.RETURNS_DEEP_STUBS); - Response response = mock(Response.class, Mockito.RETURNS_DEEP_STUBS); - - when(request.mandatoryParam("almSetting")).thenReturn("almSetting"); - when(request.param("project")).thenReturn("project"); - when(request.getMediaType()).thenReturn("dummy"); - - assertThatThrownBy(() -> testCase.handle(request, response)).isInstanceOf(NotFoundException.class).hasMessage("Project 'project' is not bound to any ALM"); - } - - @Test - public void testHandleUnknownAlmSetting() { - DbClient dbClient = mock(DbClient.class); - DbSession dbSession = mock(DbSession.class); - when(dbClient.openSession(eq(false))).thenReturn(dbSession); - ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); - ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); - when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("settingUuid"); - - ProjectDto projectDto = mock(ProjectDto.class); - when(projectDto.getKey()).thenReturn("project"); - - when(projectAlmSettingDao.selectByProject(eq(dbSession), eq(projectDto))).thenReturn(Optional.of(projectAlmSettingDto)); - when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); - - AlmSettingDao almSettingDao = mock(AlmSettingDao.class); - when(dbClient.almSettingDao()).thenReturn(almSettingDao); - when(almSettingDao.selectByKey(eq(dbSession), eq("settingUuid"))).thenReturn(Optional.empty()); - - ComponentFinder componentFinder = mock(ComponentFinder.class); - when(projectDto.getKey()).thenReturn("projectUuid"); - when(componentFinder.getProjectByKey(eq(dbSession), eq("project"))).thenReturn(projectDto); - UserSession userSession = mock(UserSession.class); - - GetBindingAction testCase = new GetBindingAction(dbClient, componentFinder, userSession); - - Request request = mock(Request.class, Mockito.RETURNS_DEEP_STUBS); - Response response = mock(Response.class, Mockito.RETURNS_DEEP_STUBS); - - when(request.mandatoryParam("almSetting")).thenReturn("almSetting"); - when(request.param("project")).thenReturn("project"); - when(request.getMediaType()).thenReturn("dummy"); - - assertThatThrownBy(() -> testCase.handle(request, response)).isInstanceOf(IllegalStateException.class).hasMessage("ALM setting 'settingUuid' cannot be found"); - - } -} \ No newline at end of file