Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

PluginFilesTest.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.scanner.bootstrap;
  21. import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
  22. import java.io.File;
  23. import java.io.IOException;
  24. import java.io.InputStream;
  25. import java.net.SocketTimeoutException;
  26. import java.nio.file.Files;
  27. import java.nio.file.Path;
  28. import java.util.Collections;
  29. import java.util.Optional;
  30. import javax.annotation.Nullable;
  31. import org.apache.commons.codec.digest.DigestUtils;
  32. import org.apache.commons.lang3.RandomStringUtils;
  33. import org.junit.jupiter.api.BeforeEach;
  34. import org.junit.jupiter.api.Test;
  35. import org.junit.jupiter.api.extension.RegisterExtension;
  36. import org.junit.jupiter.api.io.TempDir;
  37. import org.sonar.api.config.internal.MapSettings;
  38. import org.sonar.api.notifications.AnalysisWarnings;
  39. import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
  40. import org.sonar.scanner.http.DefaultScannerWsClient;
  41. import org.sonarqube.ws.client.HttpConnector;
  42. import org.sonarqube.ws.client.WsClientFactories;
  43. import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
  44. import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
  45. import static com.github.tomakehurst.wiremock.client.WireMock.exactly;
  46. import static com.github.tomakehurst.wiremock.client.WireMock.get;
  47. import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
  48. import static com.github.tomakehurst.wiremock.client.WireMock.notFound;
  49. import static com.github.tomakehurst.wiremock.client.WireMock.ok;
  50. import static com.github.tomakehurst.wiremock.client.WireMock.serverError;
  51. import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
  52. import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
  53. import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
  54. import static org.apache.commons.io.FileUtils.moveFile;
  55. import static org.assertj.core.api.Assertions.assertThat;
  56. import static org.assertj.core.api.Assertions.assertThatThrownBy;
  57. import static org.assertj.core.api.ThrowableAssert.ThrowingCallable;
  58. import static org.mockito.Mockito.mock;
  59. import static org.mockito.Mockito.when;
  60. import static org.sonar.scanner.bootstrap.PluginFiles.PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY;
  61. class PluginFilesTest {
  62. @RegisterExtension
  63. static WireMockExtension sonarqube = WireMockExtension.newInstance()
  64. .options(wireMockConfig().dynamicPort())
  65. .build();
  66. @TempDir
  67. private Path tempDir;
  68. private final SonarUserHome sonarUserHome = mock(SonarUserHome.class);
  69. private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
  70. private PluginFiles underTest;
  71. @BeforeEach
  72. void setUp(@TempDir Path sonarUserHomeDir) throws Exception {
  73. when(sonarUserHome.getPath()).thenReturn(sonarUserHomeDir);
  74. HttpConnector connector = HttpConnector.newBuilder().acceptGzip(true).url(sonarqube.url("/")).build();
  75. GlobalAnalysisMode analysisMode = new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap()));
  76. DefaultScannerWsClient wsClient = new DefaultScannerWsClient(WsClientFactories.getDefault().newClient(connector), false,
  77. analysisMode, analysisWarnings);
  78. MapSettings settings = new MapSettings();
  79. settings.setProperty(PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY, 1);
  80. underTest = new PluginFiles(wsClient, settings.asConfig(), sonarUserHome);
  81. }
  82. @Test
  83. void get_jar_from_cache_if_present() throws Exception {
  84. FileAndMd5 jar = createFileInCache("foo");
  85. File result = underTest.get(newInstalledPlugin("foo", jar.md5)).get();
  86. verifySameContent(result.toPath(), jar);
  87. // no requests to server
  88. sonarqube.verify(0, anyRequestedFor(anyUrl()));
  89. }
  90. @Test
  91. void download_and_add_jar_to_cache_if_missing() throws Exception {
  92. FileAndMd5 tempJar = new FileAndMd5();
  93. stubDownload(tempJar);
  94. InstalledPlugin plugin = newInstalledPlugin("foo", tempJar.md5);
  95. File result = underTest.get(plugin).get();
  96. verifySameContent(result.toPath(), tempJar);
  97. sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
  98. // get from cache on second call
  99. result = underTest.get(plugin).get();
  100. verifySameContent(result.toPath(), tempJar);
  101. sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
  102. }
  103. @Test
  104. void return_empty_if_plugin_not_found_on_server() {
  105. sonarqube.stubFor(get(anyUrl())
  106. .willReturn(notFound()));
  107. InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
  108. Optional<File> result = underTest.get(plugin);
  109. assertThat(result).isEmpty();
  110. }
  111. @Test
  112. void fail_if_integrity_of_download_is_not_valid() throws IOException {
  113. FileAndMd5 tempJar = new FileAndMd5();
  114. stubDownload(tempJar.file, "invalid_hash");
  115. InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
  116. expectISE("foo", "was expected to have checksum invalid_hash but had " + tempJar.md5,
  117. () -> underTest.get(plugin));
  118. }
  119. @Test
  120. void fail_if_md5_header_is_missing_from_response(@TempDir Path tempDir) throws IOException {
  121. var tempJar = Files.createTempFile(tempDir, "plugin", ".jar");
  122. stubDownload(tempJar, null);
  123. InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
  124. expectISE("foo", "did not return header Sonar-MD5", () -> underTest.get(plugin));
  125. }
  126. @Test
  127. void fail_if_server_returns_error() {
  128. sonarqube.stubFor(get(anyUrl())
  129. .willReturn(serverError()));
  130. InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
  131. expectISE("foo", "returned code 500", () -> underTest.get(plugin));
  132. }
  133. @Test
  134. void getPlugin_whenTimeOutReached_thenDownloadFails() {
  135. sonarqube.stubFor(get(anyUrl())
  136. .willReturn(ok()
  137. .withFixedDelay(5000)));
  138. InstalledPlugin plugin = newInstalledPlugin("foo", "abc");
  139. assertThatThrownBy(() -> underTest.get(plugin))
  140. .isInstanceOf(IllegalStateException.class)
  141. .hasMessageStartingWith("Fail to request url")
  142. .cause().isInstanceOf(SocketTimeoutException.class);
  143. }
  144. @Test
  145. void download_a_new_version_of_plugin_during_blue_green_switch() throws IOException {
  146. FileAndMd5 tempJar = new FileAndMd5();
  147. stubDownload(tempJar);
  148. // expecting to download plugin foo with checksum "abc"
  149. InstalledPlugin pluginV1 = newInstalledPlugin("foo", "abc");
  150. File result = underTest.get(pluginV1).get();
  151. verifySameContent(result.toPath(), tempJar);
  152. // new version of downloaded jar is put in cache with the new md5
  153. InstalledPlugin pluginV2 = newInstalledPlugin("foo", tempJar.md5);
  154. result = underTest.get(pluginV2).get();
  155. verifySameContent(result.toPath(), tempJar);
  156. sonarqube.verify(exactly(1), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
  157. // v1 still requests server and downloads v2
  158. stubDownload(tempJar);
  159. result = underTest.get(pluginV1).get();
  160. verifySameContent(result.toPath(), tempJar);
  161. sonarqube.verify(exactly(2), getRequestedFor(urlEqualTo("/api/plugins/download?plugin=foo")));
  162. }
  163. @Test
  164. void fail_if_cached_file_is_outside_cache_dir() throws IOException {
  165. FileAndMd5 tempJar = new FileAndMd5();
  166. stubDownload(tempJar);
  167. InstalledPlugin plugin = newInstalledPlugin("foo/bar", "abc");
  168. assertThatThrownBy(() -> underTest.get(plugin))
  169. .isInstanceOf(IllegalStateException.class)
  170. .hasMessage("Fail to download plugin [foo/bar]. Key is not valid.");
  171. }
  172. private FileAndMd5 createFileInCache(String pluginKey) throws IOException {
  173. FileAndMd5 tempFile = new FileAndMd5();
  174. return moveToCache(pluginKey, tempFile);
  175. }
  176. private FileAndMd5 moveToCache(String pluginKey, FileAndMd5 jar) throws IOException {
  177. Path jarInCache = sonarUserHome.getPath().resolve("cache/" + jar.md5 + "/sonar-" + pluginKey + "-plugin.jar");
  178. moveFile(jar.file.toFile(), jarInCache.toFile());
  179. return new FileAndMd5(jarInCache, jar.md5);
  180. }
  181. /**
  182. * Enqueue download of file with valid MD5
  183. */
  184. private void stubDownload(FileAndMd5 file) throws IOException {
  185. stubDownload(file.file, file.md5);
  186. }
  187. /**
  188. * Enqueue download of file with a MD5 that may not be returned (null) or not valid
  189. */
  190. private void stubDownload(Path file, @Nullable String md5) throws IOException {
  191. var responseDefBuilder = ok();
  192. if (md5 != null) {
  193. responseDefBuilder.withHeader("Sonar-MD5", md5);
  194. }
  195. responseDefBuilder.withBody(Files.readAllBytes(file));
  196. sonarqube.stubFor(get(urlMatching("/api/plugins/download\\?plugin=.*"))
  197. .willReturn(responseDefBuilder));
  198. }
  199. private static InstalledPlugin newInstalledPlugin(String pluginKey, String fileChecksum) {
  200. InstalledPlugin plugin = new InstalledPlugin();
  201. plugin.key = pluginKey;
  202. plugin.hash = fileChecksum;
  203. return plugin;
  204. }
  205. private static void verifySameContent(Path file1, FileAndMd5 file2) {
  206. assertThat(file1).isRegularFile();
  207. assertThat(file2.file).isRegularFile();
  208. assertThat(file1).hasSameTextualContentAs(file2.file);
  209. }
  210. private void expectISE(String pluginKey, String message, ThrowingCallable shouldRaiseThrowable) {
  211. assertThatThrownBy(shouldRaiseThrowable)
  212. .isInstanceOf(IllegalStateException.class)
  213. .hasMessageStartingWith("Fail to download plugin [" + pluginKey + "]")
  214. .hasMessageContaining(message);
  215. }
  216. private class FileAndMd5 {
  217. private final Path file;
  218. private final String md5;
  219. FileAndMd5(Path file, String md5) {
  220. this.file = file;
  221. this.md5 = md5;
  222. }
  223. FileAndMd5() throws IOException {
  224. this.file = Files.createTempFile(tempDir, "jar", null);
  225. Files.write(this.file, RandomStringUtils.random(3).getBytes());
  226. try (InputStream fis = Files.newInputStream(this.file)) {
  227. this.md5 = DigestUtils.md5Hex(fis);
  228. } catch (IOException e) {
  229. throw new IllegalStateException("Fail to compute md5 of " + this.file, e);
  230. }
  231. }
  232. }
  233. }