diff options
Diffstat (limited to 'tests/src/test/java/util')
24 files changed, 3233 insertions, 0 deletions
diff --git a/tests/src/test/java/util/ItUtils.java b/tests/src/test/java/util/ItUtils.java new file mode 100644 index 00000000000..3ef0d275549 --- /dev/null +++ b/tests/src/test/java/util/ItUtils.java @@ -0,0 +1,534 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +import com.google.common.base.Splitter; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.BuildResult; +import com.sonar.orchestrator.build.SonarRunner; +import com.sonar.orchestrator.container.Server; +import com.sonar.orchestrator.locator.FileLocation; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.commons.io.FileUtils; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.junit.Assert; +import org.sonar.wsclient.issue.Issue; +import org.sonar.wsclient.issue.IssueClient; +import org.sonar.wsclient.issue.IssueQuery; +import org.sonarqube.tests.Tester; +import org.sonarqube.ws.WsComponents.Component; +import org.sonarqube.ws.WsMeasures; +import org.sonarqube.ws.WsMeasures.Measure; +import org.sonarqube.ws.client.GetRequest; +import org.sonarqube.ws.client.HttpConnector; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.WsClientFactories; +import org.sonarqube.ws.client.component.ShowWsRequest; +import org.sonarqube.ws.client.measure.ComponentWsRequest; +import org.sonarqube.ws.client.qualityprofile.RestoreWsRequest; +import org.sonarqube.ws.client.setting.ResetRequest; +import org.sonarqube.ws.client.setting.SetRequest; + +import static com.google.common.base.Preconditions.checkState; +import static com.sonar.orchestrator.container.Server.ADMIN_LOGIN; +import static com.sonar.orchestrator.container.Server.ADMIN_PASSWORD; +import static java.lang.Double.parseDouble; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Locale.ENGLISH; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class ItUtils { + public static final Splitter LINE_SPLITTER = Splitter.on(System.getProperty("line.separator")); + + private ItUtils() { + } + + public static FileLocation xooPlugin() { + return FileLocation.byWildcardMavenFilename(new File("../plugins/sonar-xoo-plugin/target"), "sonar-xoo-plugin-*.jar"); + } + + public static List<Issue> getAllServerIssues(Orchestrator orchestrator) { + IssueClient issueClient = orchestrator.getServer().wsClient().issueClient(); + return issueClient.find(IssueQuery.create()).list(); + } + + /** + * @deprecated replaced by {@link Tester#wsClient()} + */ + @Deprecated + public static WsClient newAdminWsClient(Orchestrator orchestrator) { + return newUserWsClient(orchestrator, ADMIN_LOGIN, ADMIN_PASSWORD); + } + + /** + * @deprecated replaced by {@link Tester#wsClient()} + */ + @Deprecated + public static WsClient newWsClient(Orchestrator orchestrator) { + return newUserWsClient(orchestrator, null, null); + } + + /** + * @deprecated replaced by {@link Tester#wsClient()} + */ + @Deprecated + public static WsClient newUserWsClient(Orchestrator orchestrator, @Nullable String login, @Nullable String password) { + Server server = orchestrator.getServer(); + return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder() + .url(server.getUrl()) + .credentials(login, password) + .build()); + } + + /** + * Locate the directory of sample project + * + * @param relativePath path related to the directory it/projects, for example "qualitygate/xoo-sample" + */ + public static File projectDir(String relativePath) { + File dir = new File("projects/" + relativePath); + if (!dir.exists() || !dir.isDirectory()) { + throw new IllegalStateException("Directory does not exist: " + dir.getAbsolutePath()); + } + return dir; + } + + /** + * Locate the artifact of a fake plugin stored in it/plugins. + * + * @param dirName the directory of it/plugins, for example "sonar-fake-plugin". + * It assumes that version is 1.0-SNAPSHOT + */ + public static FileLocation pluginArtifact(String dirName) { + return FileLocation.byWildcardMavenFilename(new File("plugins/" + dirName + "/target"), dirName + "-*.jar"); + } + + /** + * Locate the pom file of a sample project + * + * @param projectName project path related to the directory it/projects, for example "qualitygate/xoo-sample" + */ + public static File projectPom(String projectName) { + File pom = new File(projectDir(projectName), "pom.xml"); + if (!pom.exists() || !pom.isFile()) { + throw new IllegalStateException("pom file does not exist: " + pom.getAbsolutePath()); + } + return pom; + } + + public static String sanitizeTimezones(String s) { + return s.replaceAll("[\\+\\-]\\d\\d\\d\\d", "+0000"); + } + + public static JSONObject getJSONReport(BuildResult result) { + Pattern pattern = Pattern.compile("Export issues to (.*?).json"); + Matcher m = pattern.matcher(result.getLogs()); + if (m.find()) { + String s = m.group(1); + File path = new File(s + ".json"); + assertThat(path).exists(); + try { + return (JSONObject) JSONValue.parse(FileUtils.readFileToString(path)); + } catch (IOException e) { + throw new RuntimeException("Unable to read JSON report", e); + } + } + fail("Unable to locate json report"); + return null; + } + + public static int countIssuesInJsonReport(BuildResult result, boolean onlyNews) { + JSONObject obj = getJSONReport(result); + JSONArray issues = (JSONArray) obj.get("issues"); + int count = 0; + for (Object issue : issues) { + JSONObject jsonIssue = (JSONObject) issue; + if (!onlyNews || (Boolean) jsonIssue.get("isNew")) { + count++; + } + } + return count; + } + + public static void assertIssuesInJsonReport(BuildResult result, int newIssues, int resolvedIssues, int existingIssues) { + JSONObject obj = getJSONReport(result); + JSONArray issues = (JSONArray) obj.get("issues"); + int countNew = 0; + int countResolved = 0; + int countExisting = 0; + + for (Object issue : issues) { + JSONObject jsonIssue = (JSONObject) issue; + + if ((Boolean) jsonIssue.get("isNew")) { + countNew++; + } else if (jsonIssue.get("resolution") != null) { + countResolved++; + } else { + countExisting++; + } + } + assertThat(countNew).isEqualTo(newIssues); + assertThat(countResolved).isEqualTo(resolvedIssues); + assertThat(countExisting).isEqualTo(existingIssues); + } + + public static SonarRunner runVerboseProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, String... properties) { + return runProjectAnalysis(orchestrator, projectRelativePath, true, properties); + } + + public static SonarRunner runProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, String... properties) { + return runProjectAnalysis(orchestrator, projectRelativePath, false, properties); + } + + private static SonarRunner runProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, boolean enableDebugLogs, String... properties) { + SonarRunner sonarRunner = SonarRunner.create(projectDir(projectRelativePath)); + ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); + for (int i = 0; i < properties.length; i += 2) { + builder.put(properties[i], properties[i + 1]); + } + SonarRunner scan = sonarRunner.setDebugLogs(enableDebugLogs).setProperties(builder.build()); + orchestrator.executeBuild(scan); + return scan; + } + + public static void setServerProperty(Orchestrator orchestrator, String key, @Nullable String value) { + setServerProperty(orchestrator, null, key, value); + } + + public static void setServerProperty(Orchestrator orchestrator, @Nullable String componentKey, String key, @Nullable String value) { + if (value == null) { + newAdminWsClient(orchestrator).settings().reset(ResetRequest.builder().setKeys(key).setComponent(componentKey).build()); + } else { + newAdminWsClient(orchestrator).settings().set(SetRequest.builder().setKey(key).setValue(value).setComponent(componentKey).build()); + } + } + + public static void setServerProperties(Orchestrator orchestrator, @Nullable String componentKey, String... properties) { + for (int i = 0; i < properties.length; i += 2) { + setServerProperty(orchestrator, componentKey, properties[i], properties[i + 1]); + } + } + + public static void resetSettings(Orchestrator orchestrator, @Nullable String componentKey, String... keys) { + if (keys.length > 0) { + newAdminWsClient(orchestrator).settings().reset(ResetRequest.builder().setKeys(keys).setComponent(componentKey).build()); + } + } + + public static void resetEmailSettings(Orchestrator orchestrator) { + resetSettings(orchestrator, null, "email.smtp_host.secured", "email.smtp_port.secured", "email.smtp_secure_connection.secured", "email.smtp_username.secured", + "email.smtp_password.secured", "email.from", "email.prefix"); + } + + public static void resetPeriod(Orchestrator orchestrator) { + resetSettings(orchestrator, null, "sonar.leak.period"); + } + + @CheckForNull + public static Measure getMeasure(Orchestrator orchestrator, String componentKey, String metricKey) { + return getMeasuresByMetricKey(orchestrator, componentKey, metricKey).get(metricKey); + } + + @CheckForNull + public static Double getMeasureAsDouble(Orchestrator orchestrator, String componentKey, String metricKey) { + Measure measure = getMeasure(orchestrator, componentKey, metricKey); + return (measure == null) ? null : Double.parseDouble(measure.getValue()); + } + + public static Map<String, Measure> getMeasuresByMetricKey(Orchestrator orchestrator, String componentKey, String... metricKeys) { + return getStreamMeasures(orchestrator, componentKey, metricKeys) + .filter(Measure::hasValue) + .collect(Collectors.toMap(Measure::getMetric, Function.identity())); + } + + public static Map<String, Double> getMeasuresAsDoubleByMetricKey(Orchestrator orchestrator, String componentKey, String... metricKeys) { + return getStreamMeasures(orchestrator, componentKey, metricKeys) + .filter(Measure::hasValue) + .collect(Collectors.toMap(Measure::getMetric, measure -> parseDouble(measure.getValue()))); + } + + private static Stream<Measure> getStreamMeasures(Orchestrator orchestrator, String componentKey, String... metricKeys) { + return newWsClient(orchestrator).measures().component(new ComponentWsRequest() + .setComponentKey(componentKey) + .setMetricKeys(asList(metricKeys))) + .getComponent().getMeasuresList() + .stream(); + } + + @CheckForNull + public static Measure getMeasureWithVariation(Orchestrator orchestrator, String componentKey, String metricKey) { + WsMeasures.ComponentWsResponse response = newWsClient(orchestrator).measures().component(new ComponentWsRequest() + .setComponentKey(componentKey) + .setMetricKeys(singletonList(metricKey)) + .setAdditionalFields(singletonList("periods"))); + List<Measure> measures = response.getComponent().getMeasuresList(); + return measures.size() == 1 ? measures.get(0) : null; + } + + @CheckForNull + public static Map<String, Measure> getMeasuresWithVariationsByMetricKey(Orchestrator orchestrator, String componentKey, String... metricKeys) { + return newWsClient(orchestrator).measures().component(new ComponentWsRequest() + .setComponentKey(componentKey) + .setMetricKeys(asList(metricKeys)) + .setAdditionalFields(singletonList("periods"))).getComponent().getMeasuresList() + .stream() + .collect(Collectors.toMap(Measure::getMetric, Function.identity())); + } + + /** + * Return leak period value + */ + @CheckForNull + public static Double getLeakPeriodValue(Orchestrator orchestrator, String componentKey, String metricKey) { + List<WsMeasures.PeriodValue> periodsValueList = getMeasureWithVariation(orchestrator, componentKey, metricKey).getPeriods().getPeriodsValueList(); + return periodsValueList.size() > 0 ? Double.parseDouble(periodsValueList.get(0).getValue()) : null; + } + + @CheckForNull + public static Component getComponent(Orchestrator orchestrator, String componentKey) { + try { + return newWsClient(orchestrator).components().show(new ShowWsRequest().setKey((componentKey))).getComponent(); + } catch (org.sonarqube.ws.client.HttpException e) { + if (e.code() == 404) { + return null; + } + throw new IllegalStateException(e); + } + } + + @CheckForNull + public static ComponentNavigation getComponentNavigation(Orchestrator orchestrator, String componentKey) { + // Waiting for SONAR-7745 to have version in api/components/show, we use internal api/navigation/component WS to get the component + // version + String content = newWsClient(orchestrator).wsConnector().call(new GetRequest("api/navigation/component").setParam("componentKey", componentKey)).failIfNotSuccessful() + .content(); + return ComponentNavigation.parse(content); + } + + public static void restoreProfile(Orchestrator orchestrator, URL resource) { + restoreProfile(orchestrator, resource, null); + } + + public static void restoreProfile(Orchestrator orchestrator, URL resource, String organization) { + URI uri; + try { + uri = resource.toURI(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot find quality profile xml file '" + resource + "' in classpath"); + } + newAdminWsClient(orchestrator) + .qualityProfiles() + .restoreProfile( + RestoreWsRequest.builder() + .setBackup(new File(uri)) + .setOrganization(organization) + .build()); + } + + public static String newOrganizationKey() { + return randomAlphabetic(32).toLowerCase(ENGLISH); + } + + public static String newProjectKey() { + return "key-" + randomAlphabetic(100); + } + + public static class ComponentNavigation { + private String version; + private String analysisDate; + + public String getVersion() { + return version; + } + + public Date getDate() { + return toDatetime(analysisDate); + } + + public static ComponentNavigation parse(String json) { + Gson gson = new Gson(); + return gson.fromJson(json, ComponentNavigation.class); + } + } + + /** + * Concatenates a vararg to a String array. + * + * Useful when using {@link #runVerboseProjectAnalysis(Orchestrator, String, String...)}, eg.: + * <pre> + * ItUtils.runProjectAnalysis(orchestrator, "project_name", + * ItUtils.concat(properties, "sonar.scm.disabled", "false") + * ); + * </pre> + */ + public static String[] concat(String[] properties, String... str) { + if (properties == null || properties.length == 0) { + return str; + } + return Stream.concat(Arrays.stream(properties), Arrays.stream(str)) + .toArray(String[]::new); + } + + public static Date toDate(String sDate) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.parse(sDate); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public static Date toDatetime(String sDate) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + return sdf.parse(sDate); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public static String formatDate(Date d) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(d); + } + + public static String extractCeTaskId(BuildResult buildResult) { + List<String> taskIds = extractCeTaskIds(buildResult); + checkState(taskIds.size() == 1, "More than one task id retrieved from logs"); + return taskIds.iterator().next(); + } + + private static List<String> extractCeTaskIds(BuildResult buildResult) { + String logs = buildResult.getLogs(); + return StreamSupport.stream(LINE_SPLITTER.split(logs).spliterator(), false) + .filter(s -> s.contains("More about the report processing at")) + .map(s -> s.substring(s.length() - 20, s.length())) + .collect(Collectors.toList()); + } + + public static Map<String, Object> jsonToMap(String json) { + Gson gson = new Gson(); + Type type = new TypeToken<Map<String, Object>>() { + }.getType(); + return gson.fromJson(json, type); + } + + /** + * @deprecated replaced by {@code orchestrator.getServer().newHttpCall()} + */ + @Deprecated + public static Response call(String url, String... headers) { + Request.Builder requestBuilder = new Request.Builder().get().url(url); + for (int i = 0; i < headers.length; i += 2) { + String headerName = headers[i]; + String headerValue = headers[i + 1]; + if (headerValue != null) { + requestBuilder.addHeader(headerName, headerValue); + } + } + try { + return new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + .newCall(requestBuilder.build()) + .execute(); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + public static void expectBadRequestError(Runnable runnable) { + expectHttpError(400, runnable); + } + + public static void expectMissingError(Runnable runnable) { + expectHttpError(404, runnable); + } + /** + * Missing permissions + */ + public static void expectForbiddenError(Runnable runnable) { + expectHttpError(403, runnable); + } + + /** + * Not authenticated + */ + public static void expectUnauthorizedError(Runnable runnable) { + expectHttpError(401, runnable); + } + + public static void expectNotFoundError(Runnable runnable) { + expectHttpError(404, runnable); + } + + public static void expectHttpError(int expectedCode, Runnable runnable) { + try { + runnable.run(); + Assert.fail("Ws call should have failed"); + } catch (org.sonarqube.ws.client.HttpException e) { + assertThat(e.code()).isEqualTo(expectedCode); + } + } + + public static void expectHttpError(int expectedCode, String expectedMessage, Runnable runnable) { + try { + runnable.run(); + Assert.fail("Ws call should have failed"); + } catch (org.sonarqube.ws.client.HttpException e) { + assertThat(e.code()).isEqualTo(expectedCode); + assertThat(e.getMessage()).contains(expectedMessage); + } + } +} diff --git a/tests/src/test/java/util/LoadedProfiles.java b/tests/src/test/java/util/LoadedProfiles.java new file mode 100644 index 00000000000..71c76803901 --- /dev/null +++ b/tests/src/test/java/util/LoadedProfiles.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import static com.google.common.base.Preconditions.checkArgument; + +final class LoadedProfiles { + private final Map<String, Profile> profileStatesPerProfileKey = new HashMap<>(); + + public LoadedProfiles() { + init(); + } + + public String loadProfile(String relativePathToProfile) { + try { + URL resource = getClass().getResource(relativePathToProfile); + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(resource.openStream()); + + String profileKey = null; + String languageKey = null; + + Element documentElement = document.getDocumentElement(); + checkArgument("profile".equals(documentElement.getNodeName()), "%s is not a quality profile file. Root node is not %s", resource.toURI().toString()); + NodeList childNodes = documentElement.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node childNode = childNodes.item(i); + if ("name".equals(childNode.getNodeName())) { + profileKey = childNode.getChildNodes().item(0).getNodeValue(); + } else if ("language".equals(childNode.getNodeName())) { + languageKey = childNode.getChildNodes().item(0).getNodeValue(); + } + } + checkArgument(profileKey != null, "Quality profile file %s is missing profile key", resource.toURI().toString()); + checkArgument(languageKey != null, "Quality profile file %s is missing language key", resource.toURI().toString()); + this.profileStatesPerProfileKey.put(profileKey, new Profile(profileKey, languageKey, relativePathToProfile)); + + return profileKey; + } catch (URISyntaxException | SAXException | IOException | ParserConfigurationException e) { + throw new RuntimeException("Can not load quality profile " + relativePathToProfile, e); + } + } + + public Profile getState(String qualityProfileKey) { + Profile profile = this.profileStatesPerProfileKey.get(qualityProfileKey); + checkArgument(profile != null, "Quality Profile with key %s is unknown to %s", qualityProfileKey, ProjectAnalysisRule.class.getSimpleName()); + return profile; + } + + public void reset() { + this.profileStatesPerProfileKey.clear(); + init(); + } + + private void init() { + this.profileStatesPerProfileKey.put(Profile.XOO_EMPTY_PROFILE.getProfileKey(), Profile.XOO_EMPTY_PROFILE); + } +} diff --git a/tests/src/test/java/util/LoadedProjects.java b/tests/src/test/java/util/LoadedProjects.java new file mode 100644 index 00000000000..eb3565f5f84 --- /dev/null +++ b/tests/src/test/java/util/LoadedProjects.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +import com.google.common.base.Throwables; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +final class LoadedProjects { + + static final String SONAR_PROJECT_PROPERTIES_FILE_NAME = "sonar-project.properties"; + + private final Map<String, ProjectState> projectStatePerProjectKey = new HashMap<>(); + private final Set<String> knownProjects = new HashSet<>(); + + public void reset() { + this.projectStatePerProjectKey.clear(); + this.knownProjects.clear(); + } + + public String load(String projectRelativePath) { + checkState(!knownProjects.contains(projectRelativePath), "Project at location %s already loaded", projectRelativePath); + + File projectDir = ItUtils.projectDir(projectRelativePath); + Properties sonarProjectProperties = loadProjectProperties(projectDir); + ProjectState projectState = new ProjectState(projectDir, sonarProjectProperties); + + register(projectRelativePath, projectState); + + return projectState.getProjectKey(); + } + + public ProjectState getProjectState(String projectKey) { + ProjectState projectState = this.projectStatePerProjectKey.get(projectKey); + checkArgument(projectState != null, "Project with key %s is unknown to %s", projectKey, ProjectAnalysisRule.class.getSimpleName()); + return projectState; + } + + private void register(String projectRelativePath, ProjectState projectState) { + this.projectStatePerProjectKey.put(projectState.getProjectKey(), projectState); + this.knownProjects.add(projectRelativePath); + } + + private static Properties loadProjectProperties(File projectDir) { + File sonarPropertiesFile = new File(projectDir, SONAR_PROJECT_PROPERTIES_FILE_NAME); + checkArgument(sonarPropertiesFile.exists(), "Can not locate %s in project %s", SONAR_PROJECT_PROPERTIES_FILE_NAME, projectDir.getAbsolutePath()); + + Properties properties = new Properties(); + try { + properties.load(new FileReader(sonarPropertiesFile)); + } catch (IOException e) { + Throwables.propagate(e); + } + return properties; + } +} diff --git a/tests/src/test/java/util/Profile.java b/tests/src/test/java/util/Profile.java new file mode 100644 index 00000000000..f9f6a78c52a --- /dev/null +++ b/tests/src/test/java/util/Profile.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +import javax.annotation.concurrent.Immutable; + +@Immutable +final class Profile { + static final Profile XOO_EMPTY_PROFILE = new Profile("empty", "xoo", "n/a"); + + private final String profileKey; + private final String languageKey; + private final String relativePath; + + Profile(String profileKey, String languageKey, String relativePath) { + this.profileKey = profileKey; + this.languageKey = languageKey; + this.relativePath = relativePath; + } + + public String getProfileKey() { + return profileKey; + } + + public String getLanguageKey() { + return languageKey; + } + + public String getRelativePath() { + return relativePath; + } +} diff --git a/tests/src/test/java/util/ProjectAnalysis.java b/tests/src/test/java/util/ProjectAnalysis.java new file mode 100644 index 00000000000..e60f4c37704 --- /dev/null +++ b/tests/src/test/java/util/ProjectAnalysis.java @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +public interface ProjectAnalysis { + /** + * Creates a new ProjectAnalysis which will use the specified quality profile. + * + * @throws IllegalArgumentException if the quality profile with the specified key has not been loaded into the Rule + * @see {@link ProjectAnalysisRule#registerProfile(String)} + */ + ProjectAnalysis withQualityProfile(String qualityProfileKey); + + /** + * Creates a new ProjectAnalysis which will use the built-in Xoo empty profile. + */ + ProjectAnalysis withXooEmptyProfile(); + + /** + * Creates a new ProjectAnalysis which will have debug logs enabled (or not). + */ + ProjectAnalysis withDebugLogs(boolean enabled); + + /** + * Creates a new ProjectAnalysis which will have the specified properties. + */ + ProjectAnalysis withProperties(String... properties); + + /** + * Execute the current ProjectAnalysis. + * This method can be called any number of time and will run the same analysis again and again. + */ + void run(); +} diff --git a/tests/src/test/java/util/ProjectAnalysisRule.java b/tests/src/test/java/util/ProjectAnalysisRule.java new file mode 100644 index 00000000000..89debd0c066 --- /dev/null +++ b/tests/src/test/java/util/ProjectAnalysisRule.java @@ -0,0 +1,220 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarRunner; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.junit.rules.ExternalResource; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; +import static util.ItUtils.resetSettings; + +/** + * Rule wrapping around an {@link Orchestrator} instance which handle: + * <ul> + * <li>automatic reset of Orchestrator data after each method when used as a {@link org.junit.Rule}, + * after each class when used as a {@link org.junit.ClassRule}</li> + * <li>automatic reset of server properties after each method when used as a {@link org.junit.Rule}, + * after each class when used as a {@link org.junit.ClassRule}</li> + * <li>associating project with a specific Quality Profile before running an analysis</li> + * <li>provisioning a project before its first analysis so that a Quality Profile can be associated to it</li> + * <li>"restoring" a Quality Profile before an analysis with a specific Quality Profile</li> + * </ul> + * + * This Rule has preparatory methods ({@link #registerProfile(String)} and {@link #registerProject(String)}) which + * will allow consequent calls to the rule methods to be based solely on Quality Profile and Project keys. In addition, + * these methods returns the Quality Profile and Project key to avoid information duplication (the only magic string + * the IT developer has to know is the relative path of the Project or the Quality Profile). + * + * To run an analysis, use method {@link #newProjectAnalysis(String)} to create a {@link ProjectAnalysis} + * object. This object has a {@link ProjectAnalysis#run()} method which will start the analysis. + * {@link ProjectAnalysis} can safely be reused to run the same analysis multiple times. In addition, these objects are + * immutable. Any call to one of their method which would modify their state will create a new instance which can also + * be reused at will. + */ +public class ProjectAnalysisRule extends ExternalResource { + + private final Orchestrator orchestrator; + private final LoadedProfiles loadedProfiles = new LoadedProfiles(); + private final LoadedProjects loadedProjects = new LoadedProjects(); + private final Set<String> serverProperties = new HashSet<>(); + + private ProjectAnalysisRule(Orchestrator orchestrator) { + this.orchestrator = orchestrator; + } + + public static ProjectAnalysisRule from(Orchestrator orchestrator) { + return new ProjectAnalysisRule(requireNonNull(orchestrator, "Orchestrator instance can not be null")); + } + + /** + * @param relativePathToProfile eg.: "/issue/suite/IssueFilterExtensionTest/xoo-with-many-rules.xml" + * + * @return the quality profile key + */ + public String registerProfile(String relativePathToProfile) { + return this.loadedProfiles.loadProfile(relativePathToProfile); + } + + /** + * @param projectRelativePath path relative to it/projects, eg. "shared/xoo-multi-modules-sample" + * + * @return the project key + */ + public String registerProject(String projectRelativePath) { + return this.loadedProjects.load(projectRelativePath); + } + + public ProjectAnalysis newProjectAnalysis(String projectKey) { + ProjectState projectState = this.loadedProjects.getProjectState(projectKey); + + return new ProjectAnalysisImpl(projectState, null, false); + } + + @Override + protected void before() throws Throwable { + orchestrator.resetData(); + } + + @Override + protected void after() { + resetServerProperties(); + resetRuleState(); + } + + private void resetServerProperties() { + resetSettings(orchestrator, null, serverProperties.toArray(new String[] {})); + } + + public void setServerPropertyImpl(String key, @Nullable String value) { + ItUtils.setServerProperty(orchestrator, key, value); + } + + public ProjectAnalysisRule setServerProperty(String key, String value) { + setServerPropertyImpl(key, value); + this.serverProperties.add(key); + return this; + } + + @Immutable + private final class ProjectAnalysisImpl implements ProjectAnalysis { + private final ProjectState projectState; + @CheckForNull + private final Profile qualityProfile; + private final boolean debugLogs; + @CheckForNull + private final String[] properties; + + private ProjectAnalysisImpl(ProjectState projectState, @Nullable Profile qualityProfile, boolean debugLogs, String... properties) { + this.projectState = projectState; + this.qualityProfile = qualityProfile; + this.debugLogs = debugLogs; + this.properties = properties; + } + + @Override + public ProjectAnalysis withQualityProfile(String qualityProfileKey) { + checkNotNull(qualityProfileKey, "Specified Quality Profile Key can not be null"); + if (this.qualityProfile != null && this.qualityProfile.getProfileKey().equals(qualityProfileKey)) { + return this; + } + + return new ProjectAnalysisImpl(this.projectState, loadedProfiles.getState(qualityProfileKey), this.debugLogs, this.properties); + } + + @Override + public ProjectAnalysis withXooEmptyProfile() { + if (this.qualityProfile == Profile.XOO_EMPTY_PROFILE) { + return this; + } + return new ProjectAnalysisImpl(this.projectState, Profile.XOO_EMPTY_PROFILE, this.debugLogs, this.properties); + } + + @Override + public ProjectAnalysis withDebugLogs(boolean enabled) { + if (this.debugLogs == enabled) { + return this; + } + return new ProjectAnalysisImpl(this.projectState, this.qualityProfile, enabled, this.properties); + } + + @Override + public ProjectAnalysis withProperties(String... properties) { + checkArgument( + properties == null || properties.length % 2 == 0, + "there must be an even number of String parameters (got %s): key/value pairs must be complete", properties == null ? 0 : properties.length); + return new ProjectAnalysisImpl(this.projectState, this.qualityProfile, this.debugLogs, properties); + } + + @Override + public void run() { + provisionIfNecessary(); + setQualityProfileIfNecessary(); + runAnalysis(); + } + + private void setQualityProfileIfNecessary() { + if (this.qualityProfile != null) { + if (this.qualityProfile != Profile.XOO_EMPTY_PROFILE) { + ItUtils.restoreProfile(orchestrator, getClass().getResource(this.qualityProfile.getRelativePath())); + } + orchestrator.getServer().associateProjectToQualityProfile( + this.projectState.getProjectKey(), + this.qualityProfile.getLanguageKey(), + this.qualityProfile.getProfileKey()); + } + } + + private void provisionIfNecessary() { + if (this.qualityProfile != null && !projectState.isProvisioned()) { + String projectKey = projectState.getProjectKey(); + orchestrator.getServer().provisionProject(projectKey, MoreObjects.firstNonNull(projectState.getProjectName(), projectKey)); + projectState.setProvisioned(true); + } + } + + private SonarRunner runAnalysis() { + SonarRunner sonarRunner = SonarRunner.create(projectState.getProjectDir()); + ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); + for (int i = 0; i < this.properties.length; i += 2) { + builder.put(this.properties[i], this.properties[i + 1]); + } + SonarRunner scan = sonarRunner.setDebugLogs(this.debugLogs).setProperties(builder.build()); + orchestrator.executeBuild(scan); + return scan; + } + } + + private void resetRuleState() { + this.loadedProjects.reset(); + this.loadedProfiles.reset(); + this.serverProperties.clear(); + } + +} diff --git a/tests/src/test/java/util/ProjectState.java b/tests/src/test/java/util/ProjectState.java new file mode 100644 index 00000000000..cfa310326b4 --- /dev/null +++ b/tests/src/test/java/util/ProjectState.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util; + +import java.io.File; +import java.util.Properties; + +import static com.google.common.base.Preconditions.checkState; +import static util.LoadedProjects.SONAR_PROJECT_PROPERTIES_FILE_NAME; + +final class ProjectState { + private static final String SONAR_PROJECT_KEY_PROPERTY_NAME = "sonar.projectKey"; + private static final String SONAR_PROJECT_NAME_PROPERTY_NAME = "sonar.projectName"; + + private final File projectDir; + private final Properties properties; + private boolean provisioned = false; + + ProjectState(File projectDir, Properties properties) { + this.projectDir = projectDir; + this.properties = properties; + } + + public File getProjectDir() { + return projectDir; + } + + public Properties getProperties() { + return properties; + } + + public String getProjectKey() { + return getProperty(SONAR_PROJECT_KEY_PROPERTY_NAME); + } + + public String getProjectName() { + return getProperty(SONAR_PROJECT_NAME_PROPERTY_NAME); + } + + private String getProperty(String propertyName) { + String value = this.properties.getProperty(propertyName); + checkState(value != null, "Property %s is missing in %s file in project directory %s", + propertyName, SONAR_PROJECT_PROPERTIES_FILE_NAME, projectDir.getAbsolutePath()); + return value; + } + + public boolean isProvisioned() { + return provisioned; + } + + public void setProvisioned(boolean provisioned) { + this.provisioned = provisioned; + } +} diff --git a/tests/src/test/java/util/issue/IssueRule.java b/tests/src/test/java/util/issue/IssueRule.java new file mode 100644 index 00000000000..7857d3cabdc --- /dev/null +++ b/tests/src/test/java/util/issue/IssueRule.java @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.issue; + +import com.sonar.orchestrator.Orchestrator; +import java.util.List; +import org.junit.rules.ExternalResource; +import org.sonarqube.ws.Issues.Issue; +import org.sonarqube.ws.Issues.SearchWsResponse; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.issue.SearchWsRequest; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.newAdminWsClient; + +public class IssueRule extends ExternalResource { + + private final Orchestrator orchestrator; + + private WsClient adminWsClient; + + private IssueRule(Orchestrator orchestrator) { + this.orchestrator = orchestrator; + } + + public static IssueRule from(Orchestrator orchestrator) { + return new IssueRule(requireNonNull(orchestrator, "Orchestrator instance can not be null")); + } + + public SearchWsResponse search(SearchWsRequest request) { + return adminWsClient().issues().search(request); + } + + public Issue getRandomIssue() { + List<Issue> issues = search(new SearchWsRequest()).getIssuesList(); + assertThat(issues).isNotEmpty(); + return issues.get(0); + } + + public Issue getByKey(String issueKey) { + List<Issue> issues = search(new SearchWsRequest().setIssues(singletonList(issueKey)).setAdditionalFields(singletonList("_all"))).getIssuesList(); + assertThat(issues).hasSize(1); + return issues.iterator().next(); + } + + public List<Issue> getByKeys(String... issueKeys) { + List<Issue> issues = search(new SearchWsRequest().setIssues(asList(issueKeys)).setAdditionalFields(singletonList("_all"))).getIssuesList(); + assertThat(issues).hasSize(issueKeys.length); + return issues; + } + + private WsClient adminWsClient() { + if (adminWsClient == null) { + adminWsClient = newAdminWsClient(orchestrator); + } + return adminWsClient; + } + +} diff --git a/tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java b/tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java new file mode 100644 index 00000000000..364ba779d6e --- /dev/null +++ b/tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import org.openqa.selenium.By; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.internal.FindsByCssSelector; +import org.openqa.selenium.internal.FindsById; +import org.openqa.selenium.internal.FindsByName; + +public class ByCssSelectorOrByNameOrById extends By implements Serializable { + private static final long serialVersionUID = -3910258723099459239L; + + private final String selector; + + public ByCssSelectorOrByNameOrById(String selector) { + this.selector = selector; + } + + @Override + public WebElement findElement(SearchContext context) { + WebElement element; + + if (validCssSelector(selector)) { + element = ((FindsByCssSelector) context).findElementByCssSelector(quoteCss(selector)); + if (element != null) { + return element; + } + } + + element = ((FindsByName) context).findElementByName(selector); + if (element != null) { + return element; + } + + element = ((FindsById) context).findElementById(selector); + if (element != null) { + return element; + } + + return null; + } + + @Override + public List<WebElement> findElements(SearchContext context) { + List<WebElement> elements; + + if (validCssSelector(selector)) { + elements = ((FindsByCssSelector) context).findElementsByCssSelector(quoteCss(selector)); + if ((elements != null) && (!elements.isEmpty())) { + return elements; + } + } + + elements = ((FindsByName) context).findElementsByName(selector); + if ((elements != null) && (!elements.isEmpty())) { + return elements; + } + + elements = ((FindsById) context).findElementsById(selector); + if ((elements != null) && (!elements.isEmpty())) { + return elements; + } + + return Collections.emptyList(); + } + + protected boolean validCssSelector(String selector) { + return !selector.endsWith("[]"); + } + + protected String quoteCss(String selector) { + if (selector.startsWith(".")) { + return selector; + } + if (selector.startsWith("#")) { + return selector.replaceAll("(\\w)[.]", "$1\\\\."); + } + return selector; + } + + @Override + public String toString() { + return selector; + } +} diff --git a/tests/src/test/java/util/selenium/Consumer.java b/tests/src/test/java/util/selenium/Consumer.java new file mode 100644 index 00000000000..69009893a97 --- /dev/null +++ b/tests/src/test/java/util/selenium/Consumer.java @@ -0,0 +1,24 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +public interface Consumer<T> { + void accept(T t); +} diff --git a/tests/src/test/java/util/selenium/ElementFilter.java b/tests/src/test/java/util/selenium/ElementFilter.java new file mode 100644 index 00000000000..86b42cfa826 --- /dev/null +++ b/tests/src/test/java/util/selenium/ElementFilter.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.google.common.base.Function; +import java.util.Collection; +import org.openqa.selenium.WebElement; + +class ElementFilter { + private static final ElementFilter ANY = new ElementFilter("", new Function<Collection<WebElement>, Collection<WebElement>>() { + @Override + public Collection<WebElement> apply(Collection<WebElement> input) { + return input; + } + }); + + private final String description; + private final Function<Collection<WebElement>, Collection<WebElement>> filter; + + ElementFilter(String description, Function<Collection<WebElement>, Collection<WebElement>> filter) { + this.description = description; + this.filter = filter; + } + + public String getDescription() { + return description; + } + + public Function<Collection<WebElement>, Collection<WebElement>> getFilter() { + return filter; + } + + public static ElementFilter any() { + return ANY; + } + + public ElementFilter and(final ElementFilter second) { + if (ANY == this) { + return second; + } + if (ANY == second) { + return this; + } + return new ElementFilter(description + ',' + second.description, new Function<Collection<WebElement>, Collection<WebElement>>() { + @Override + public Collection<WebElement> apply(Collection<WebElement> stream) { + return second.filter.apply(filter.apply(stream)); + } + }); + } +} diff --git a/tests/src/test/java/util/selenium/Failure.java b/tests/src/test/java/util/selenium/Failure.java new file mode 100644 index 00000000000..8ba36543820 --- /dev/null +++ b/tests/src/test/java/util/selenium/Failure.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import java.util.ArrayList; +import java.util.List; + +class Failure { + private static final String PREFIX = Failure.class.getPackage().getName() + "."; + + private Failure() { + // Static class + } + + public static AssertionError create(String message) { + AssertionError error = new AssertionError(message); + removeSimpleleniumFromStackTrace(error); + return error; + } + + private static void removeSimpleleniumFromStackTrace(Throwable throwable) { + List<StackTraceElement> filtered = new ArrayList<>(); + + for (StackTraceElement element : throwable.getStackTrace()) { + if (!element.getClassName().contains(PREFIX)) { + filtered.add(element); + } + } + + throwable.setStackTrace(filtered.toArray(new StackTraceElement[filtered.size()])); + } +} diff --git a/tests/src/test/java/util/selenium/LazyDomElement.java b/tests/src/test/java/util/selenium/LazyDomElement.java new file mode 100644 index 00000000000..27c5199f820 --- /dev/null +++ b/tests/src/test/java/util/selenium/LazyDomElement.java @@ -0,0 +1,174 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.google.common.base.Function; +import com.google.common.base.Predicate; +import com.google.common.base.Supplier; +import com.google.common.collect.FluentIterable; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; +import javax.annotation.Nullable; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.Select; + +class LazyDomElement { + private final WebDriver driver; + private final By selector; + private final ElementFilter filter; + private final Retry retry; + + LazyDomElement(WebDriver driver, By selector) { + this(driver, selector, Retry._30_SECONDS); + } + + LazyDomElement(WebDriver driver, By selector, Retry retry) { + this(driver, selector, ElementFilter.any(), retry); + } + + private LazyDomElement(WebDriver driver, By selector, ElementFilter filter, Retry retry) { + this.driver = driver; + this.selector = selector; + this.filter = filter; + this.retry = retry; + } + + public LazyDomElement withText(final String text) { + String fullDescription = " with text [" + text + "]"; + + return with(new ElementFilter(fullDescription, new Function<Collection<WebElement>, Collection<WebElement>>() { + @Override + public Collection<WebElement> apply(Collection<WebElement> stream) { + return FluentIterable.from(stream).filter(new Predicate<WebElement>() { + @Override + public boolean apply(@Nullable WebElement element) { +// return Objects.equals(element.getText(), text); + return element.getText().contains(text); + } + }).toList(); + } + })); + } + + public LazyShould should() { + return new LazyShould(this, Retry._30_SECONDS, true); + } + + public void fill(final CharSequence text) { + execute("fill(" + text + ")", new Consumer<WebElement>() { + @Override + public void accept(WebElement element) { + element.clear(); + element.sendKeys(text); + } + }); + } + + public void pressEnter() { + execute("pressEnter", new Consumer<WebElement>() { + @Override + public void accept(WebElement element) { + element.sendKeys(Keys.ENTER); + } + }); + } + + public void select(final String text) { + executeSelect("select(" + text + ")", new Consumer<Select>() { + @Override + public void accept(Select select) { + select.selectByVisibleText(text); + } + }); + } + + public void executeSelect(String description, final Consumer<Select> selectOnElement) { + execute(description, new Consumer<WebElement>() { + @Override + public void accept(WebElement element) { + selectOnElement.accept(new Select(element)); + } + }); + } + + public void click() { + execute("click", new Consumer<WebElement>() { + @Override + public void accept(WebElement element) { + new Actions(driver).moveToElement(element); + element.click(); + } + }); + } + + public void check() { + execute("check", new Consumer<WebElement>() { + @Override + public void accept(WebElement element) { + if (!element.isSelected()) { + element.click(); + } + } + }); + } + + public void execute(Consumer<WebElement> action) { + execute("execute(" + action + ")", action); + } + + private LazyDomElement with(ElementFilter filter) { + return new LazyDomElement(driver, selector, this.filter.and(filter), retry); + } + + private void execute(String message, Consumer<WebElement> action) { + System.out.println(" - " + Text.toString(selector) + filter.getDescription() + "." + message); + + Supplier<Optional<WebElement>> findOne = new Supplier<Optional<WebElement>>() { + @Override + public Optional<WebElement> get() { + List<WebElement> elements = stream(); + if (elements.isEmpty()) { + return Optional.empty(); + } + return Optional.of(elements.get(0)); + } + }; + + try { + retry.execute(findOne, action); + } catch (NoSuchElementException e) { + throw new AssertionError("Element not found: " + Text.toString(selector)); + } + } + + List<WebElement> stream() { + return FluentIterable.from(filter.getFilter().apply(driver.findElements(selector))).toList(); + } + + @Override + public String toString() { + return Text.toString(selector) + filter.getDescription(); + } +} diff --git a/tests/src/test/java/util/selenium/LazyShould.java b/tests/src/test/java/util/selenium/LazyShould.java new file mode 100644 index 00000000000..c880a7d944b --- /dev/null +++ b/tests/src/test/java/util/selenium/LazyShould.java @@ -0,0 +1,190 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.base.Supplier; +import com.google.common.collect.FluentIterable; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.regex.Pattern; +import javax.annotation.Nullable; +import org.openqa.selenium.WebElement; + +class LazyShould { + private final LazyDomElement element; + private final Retry retry; + private final boolean ok; + + LazyShould(LazyDomElement element, Retry retry, boolean ok) { + this.element = element; + this.retry = retry; + this.ok = ok; + } + + public LazyShould beDisplayed() { + return verify( + isOrNot("displayed"), + new Predicate<List<WebElement>>() { + @Override + public boolean apply(List<WebElement> elements) { + return !elements.isEmpty() && FluentIterable.from(elements).allMatch(new Predicate<WebElement>() { + @Override + public boolean apply(WebElement element) { + return element.isDisplayed(); + } + }); + } + }, + new Function<List<WebElement>, String>() { + @Override + public String apply(List<WebElement> elements) { + return "It is " + statuses(elements, new Function<WebElement, String>() { + @Override + public String apply(WebElement element) { + return displayedStatus(element); + } + }); + } + }); + } + + public LazyShould match(final Pattern regexp) { + return verify( + doesOrNot("match") + " (" + regexp.pattern() + ")", + new Predicate<List<WebElement>>() { + @Override + public boolean apply(List<WebElement> elements) { + return !elements.isEmpty() && FluentIterable.from(elements).anyMatch(new Predicate<WebElement>() { + @Override + public boolean apply(WebElement element) { + return regexp.matcher(WebElementHelper.text(element)).matches(); + } + }); + } + }, + new Function<List<WebElement>, String>() { + @Override + public String apply(List<WebElement> elements) { + return "It contains " + statuses(elements, new Function<WebElement, String>() { + @Nullable + @Override + public String apply(@Nullable WebElement element) { + return WebElementHelper.text(element); + } + }); + } + }); + } + + public LazyShould contain(final String text) { + return verify( + doesOrNot("contain") + "(" + text + ")", + new Predicate<List<WebElement>>() { + @Override + public boolean apply(List<WebElement> elements) { + return FluentIterable.from(elements).anyMatch(new Predicate<WebElement>() { + @Override + public boolean apply(@Nullable WebElement element) { + if (text.startsWith("exact:")) { + return WebElementHelper.text(element).equals(text.substring(6)); + } + return WebElementHelper.text(element).contains(text); + } + }); + } + }, + new Function<List<WebElement>, String>() { + @Override + public String apply(List<WebElement> elements) { + return "It contains " + statuses(elements, new Function<WebElement, String>() { + @Override + public String apply(WebElement element) { + return WebElementHelper.text(element); + } + }); + } + }); + } + + public LazyShould exist() { + return verify( + doesOrNot("exist"), + new Predicate<List<WebElement>>() { + @Override + public boolean apply(List<WebElement> elements) { + return !elements.isEmpty(); + } + }, + new Function<List<WebElement>, String>() { + @Override + public String apply(List<WebElement> elements) { + return "It contains " + Text.plural(elements.size(), "element"); + } + }); + } + + private static String displayedStatus(WebElement element) { + return element.isDisplayed() ? "displayed" : "not displayed"; + } + + private LazyShould verify(String message, Predicate<List<WebElement>> predicate, Function<List<WebElement>, String> toErrorMessage) { + String verification = "verify that " + element + " " + message; + System.out.println(" -> " + verification); + + try { + if (!retry.verify(new Supplier<List<WebElement>>() { + @Override + public List<WebElement> get() { + return LazyShould.this.findElements(); + } + }, ok ? predicate : Predicates.not(predicate))) { + throw Failure.create("Failed to " + verification + ". " + toErrorMessage.apply(findElements())); + } + } catch (NoSuchElementException e) { + throw Failure.create("Element not found. Failed to " + verification); + } + + return ok ? this : not(); + } + + private List<WebElement> findElements() { + return element.stream(); + } + + private static String statuses(List<WebElement> elements, Function<WebElement, String> toStatus) { + return "(" + FluentIterable.from(elements).transform(toStatus).join(Joiner.on(";")) + ")"; + } + + public LazyShould not() { + return new LazyShould(element, retry, !ok); + } + + private String doesOrNot(String verb) { + return Text.doesOrNot(!ok, verb); + } + + private String isOrNot(String state) { + return Text.isOrNot(!ok, state); + } +} diff --git a/tests/src/test/java/util/selenium/Optional.java b/tests/src/test/java/util/selenium/Optional.java new file mode 100644 index 00000000000..918d488a85f --- /dev/null +++ b/tests/src/test/java/util/selenium/Optional.java @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import java.util.NoSuchElementException; + +public final class Optional<T> { + private static final Optional<?> EMPTY = new Optional<>(); + + private final T value; + + private Optional() { + this.value = null; + } + + public static <T> Optional<T> empty() { + @SuppressWarnings("unchecked") + Optional<T> t = (Optional<T>) EMPTY; + return t; + } + + private Optional(T value) { + this.value = value; + } + + public static <T> Optional<T> of(T value) { + return new Optional<>(value); + } + + public T get() { + if (value == null) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + public boolean isPresent() { + return value != null; + } +} diff --git a/tests/src/test/java/util/selenium/Retry.java b/tests/src/test/java/util/selenium/Retry.java new file mode 100644 index 00000000000..7cc8a20897c --- /dev/null +++ b/tests/src/test/java/util/selenium/Retry.java @@ -0,0 +1,152 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.google.common.base.Predicate; +import com.google.common.base.Supplier; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; +import org.openqa.selenium.InvalidElementStateException; +import org.openqa.selenium.NotFoundException; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.WebDriverException; + +import static java.util.concurrent.TimeUnit.SECONDS; + +class Retry { + public static final Retry _30_SECONDS = new Retry(30, SECONDS); + + private final long timeoutInMs; + + Retry(long duration, TimeUnit timeUnit) { + this.timeoutInMs = timeUnit.toMillis(duration); + } + + <T> void execute(Supplier<Optional<T>> target, Consumer<T> action) { + WebDriverException lastError = null; + + boolean retried = false; + + long start = System.currentTimeMillis(); + while ((System.currentTimeMillis() - start) < timeoutInMs) { + try { + Optional<T> targetElement = target.get(); + if (targetElement.isPresent()) { + action.accept(targetElement.get()); + if (retried) { + System.out.println(); + } + return; + } + } catch (StaleElementReferenceException e) { + // ignore + } catch (WebDriverException e) { + lastError = e; + } + + retried = true; + System.out.print("."); + } + + if (retried) { + System.out.println(); + } + + if (lastError != null) { + throw lastError; + } + throw new NoSuchElementException("Not found"); + } + + <T> void execute(Runnable action) { + WebDriverException lastError = null; + + boolean retried = false; + + long start = System.currentTimeMillis(); + while ((System.currentTimeMillis() - start) < timeoutInMs) { + try { + action.run(); + if (retried) { + System.out.println(); + } + return; + } catch (StaleElementReferenceException e) { + // ignore + } catch (WebDriverException e) { + lastError = e; + } + + retried = true; + System.out.print("."); + } + + if (retried) { + System.out.println(); + } + + if (lastError != null) { + throw lastError; + } + throw new NoSuchElementException("Not found"); + } + + <T> boolean verify(Supplier<T> targetSupplier, Predicate<T> predicate) throws NoSuchElementException { + Error error = Error.KO; + + boolean retried = false; + + long start = System.currentTimeMillis(); + while ((System.currentTimeMillis() - start) < timeoutInMs) { + try { + if (predicate.apply(targetSupplier.get())) { + if (retried) { + System.out.println(); + } + return true; + } + + error = Error.KO; + } catch (InvalidElementStateException e) { + error = Error.KO; + } catch (NotFoundException e) { + error = Error.NOT_FOUND; + } catch (StaleElementReferenceException e) { + // ignore + } + + retried = true; + System.out.print("."); + } + + if (retried) { + System.out.println(); + } + + if (error == Error.NOT_FOUND) { + throw new NoSuchElementException("Not found"); + } + return false; + } + + enum Error { + NOT_FOUND, KO + } +} diff --git a/tests/src/test/java/util/selenium/Selenese.java b/tests/src/test/java/util/selenium/Selenese.java new file mode 100644 index 00000000000..187e33d9024 --- /dev/null +++ b/tests/src/test/java/util/selenium/Selenese.java @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.sonar.orchestrator.Orchestrator; +import java.io.File; +import javax.annotation.Nullable; +import org.apache.commons.io.FileUtils; +import org.sonarqube.tests.Tester; + +/** + * Selenium HTML tests, generally written with Selenium IDE + * @deprecated replaced by {@link Tester} + */ +@Deprecated +public final class Selenese { + + private File[] htmlTests; + + public Selenese(Builder builder) { + this.htmlTests = builder.htmlTests; + } + + public File[] getHtmlTests() { + return htmlTests; + } + + /** + * @deprecated replaced by {@link Tester#runHtmlTests(String...)} + */ + @Deprecated + public static void runSelenese(Orchestrator orchestrator, String... htmlFiles) { + Selenese selenese = new Builder() + .setHtmlTests(htmlFiles) + .build(); + new SeleneseRunner().runOn(selenese, orchestrator); + } + + private static final class Builder { + private File[] htmlTests; + + private Builder() { + } + + public Builder setHtmlTests(File... htmlTests) { + this.htmlTests = htmlTests; + return this; + } + + public Builder setHtmlTests(String... htmlTestPaths) { + this.htmlTests = new File[htmlTestPaths.length]; + for (int index = 0; index < htmlTestPaths.length; index++) { + htmlTests[index] = FileUtils.toFile(getClass().getResource(htmlTestPaths[index])); + } + return this; + } + + public Selenese build() { + if (htmlTests == null || htmlTests.length == 0) { + throw new IllegalArgumentException("HTML suite or tests are missing"); + } + for (File htmlTest : htmlTests) { + checkPresence(htmlTest); + } + return new Selenese(this); + } + + private static void checkPresence(@Nullable File file) { + if (file == null || !file.isFile() || !file.exists()) { + throw new IllegalArgumentException("HTML file does not exist: " + file); + } + } + } +} diff --git a/tests/src/test/java/util/selenium/SeleneseRunner.java b/tests/src/test/java/util/selenium/SeleneseRunner.java new file mode 100644 index 00000000000..96cc4e609a2 --- /dev/null +++ b/tests/src/test/java/util/selenium/SeleneseRunner.java @@ -0,0 +1,440 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.sonar.orchestrator.Orchestrator; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; +import org.assertj.core.util.Strings; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NotFoundException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.logging.LogEntries; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import org.sonarqube.pageobjects.SelenideConfig; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; +import static java.util.regex.Pattern.DOTALL; +import static org.assertj.core.api.Assertions.assertThat; +import static util.selenium.Retry._30_SECONDS; + +class SeleneseRunner { + + private Map<String, String> variables; + private String baseUrl; + private WebDriver driver; + + void runOn(Selenese selenese, Orchestrator orchestrator) { + this.variables = new HashMap<>(); + this.baseUrl = orchestrator.getServer().getUrl(); + this.driver = SelenideConfig.configure(orchestrator); + + driver.manage().deleteAllCookies(); + + for (File file : selenese.getHtmlTests()) { + System.out.println(); + System.out.println("============ " + file.getName() + " ============"); + Document doc = parse(file); + for (Element table : doc.getElementsByTag("table")) { + for (Element tbody : table.getElementsByTag("tbody")) { + for (Element tr : tbody.getElementsByTag("tr")) { + String action = tr.child(0).text(); + String param1 = tr.child(1).text(); + String param2 = tr.child(2).text(); + + try { + action(action, param1, param2); + } catch (AssertionError e) { + analyzeLog(driver); + throw e; + } + } + } + } + } + } + + private static void analyzeLog(WebDriver driver) { + LogEntries logEntries = driver.manage().logs().get(LogType.BROWSER); + for (LogEntry entry : logEntries) { + System.out.println(new Date(entry.getTimestamp()) + " " + entry.getLevel() + " " + entry.getMessage()); + } + } + + private static Document parse(File file) { + try { + return Jsoup.parse(file, UTF_8.name()); + } catch (IOException e) { + throw new RuntimeException("Unable to parse file: " + file, e); + } + } + + public SeleneseRunner action(String action, String param1, String param2) { + switch (action) { + case "open": + open(param1); + return this; + case "type": + type(param1, param2); + return this; + case "keyPressAndWait": + keyPressAndWait(param1, param2); + return this; + case "select": + select(param1, param2); + return this; + case "clickAndWait": + case "click": + click(param1); + return this; + case "check": + check(param1); + return this; + case "selectFrame": + selectFrame(param1); + return this; + case "assertElementPresent": + assertElementPresent(param1); + return this; + case "assertElementNotPresent": + assertElementNotPresent(param1); + return this; + case "storeText": + storeText(param1, param2); + return this; + case "storeEval": + storeEval(param1, param2); + return this; + case "store": + store(param1, param2); + return this; + case "assertText": + case "waitForText": + assertText(param1, param2); + return this; + case "assertNotText": + case "waitForNotText": + assertNotText(param1, param2); + return this; + case "assertTextPresent": + assertTextPresent(param1); + return this; + case "assertTextNotPresent": + assertTextNotPresent(param1); + return this; + case "assertLocation": + assertLocation(param1); + return this; + case "verifyHtmlSource": + verifyHtmlSource(param1); + return this; + case "waitForElementPresent": + waitForElementPresent(param1, param2); + return this; + case "waitForElementNotPresent": + waitForElementNotPresent(param1, param2); + return this; + case "waitForVisible": + waitForVisible(param1); + return this; + case "waitForXpathCount": + waitForXpathCount(param1, Integer.parseInt(param2)); + return this; + case "assertValue": + case "waitForValue": + case "verifyValue": + assertInputValue(param1, param2); + return this; + case "assertConfirmation": + confirm(param1); + return this; + case "setTimeout": + case "pause": + // Ignore + return this; + } + + throw new IllegalArgumentException("Unsupported action: " + action); + } + + private void open(String url) { + if (url.startsWith("/sonar/")) { + goTo(url.substring(6)); + } else { + goTo(url); + } + } + + private void goTo(String url) { + requireNonNull(url, "The url cannot be null"); + + url = replacePlaceholders(url); + + URI uri = URI.create(url.replace(" ", "%20").replace("|", "%7C")); + if (!uri.isAbsolute()) { + url = baseUrl + url; + } + + System.out.println("goTo " + url); + driver.get(url); + System.out.println(" - current url " + driver.getCurrentUrl()); + } + + private LazyDomElement find(String selector) { + selector = replacePlaceholders(selector); + + if (selector.startsWith("link=") || selector.startsWith("Link=")) { + return find("a").withText(selector.substring(5)); + } + + By by; + if (selector.startsWith("//")) { + by = new By.ByXPath(selector); + } else if (selector.startsWith("xpath=")) { + by = new By.ByXPath(selector.substring(6)); + } else if (selector.startsWith("id=")) { + by = new By.ById(selector.substring(3)); + } else if (selector.startsWith("name=")) { + by = new By.ByName(selector.substring(5)); + } else if (selector.startsWith("css=")) { + by = new By.ByCssSelector(selector.substring(4)); + } else if (selector.startsWith("class=")) { + by = new By.ByCssSelector("." + selector.substring(6)); + } else { + by = new ByCssSelectorOrByNameOrById(selector); + } + + return new LazyDomElement(driver, by); + } + + private void click(String selector) { + find(selector).click(); + } + + private void check(String selector) { + find(selector).check(); + } + + private void selectFrame(final String id) { + if ("relative=parent".equals(id)) { + return; + } + + System.out.println(" - selectFrame(" + id + ")"); + _30_SECONDS.execute(new Runnable() { + @Override + public void run() { + driver.switchTo().frame(id); + } + }); + } + + private void type(String selector, String text) { + find(selector).fill(replacePlaceholders(text)); + } + + private void keyPressAndWait(String selector, String key) { + if (!key.equals("\\13")) { + throw new IllegalArgumentException("Invalid key: " + key); + } + find(selector).pressEnter(); + } + + private void select(String selector, String text) { + if (text.startsWith("label=")) { + find(selector).select(text.substring(6)); + } else { + find(selector).select(text); + } + } + + private void assertElementPresent(String selector) { + find(selector).should().beDisplayed(); + } + + private void assertElementNotPresent(String selector) { + find(selector).should().not().beDisplayed(); + } + + private void storeText(String selector, String name) { + find(selector).execute(new ExtractVariable(name)); + } + + private void storeEval(final String expression, final String name) { + // Retry until it's not null and doesn't fail + _30_SECONDS.execute(new Runnable() { + @Override + public void run() { + Object result = ((JavascriptExecutor) driver).executeScript("return " + expression); + if (result == null) { + throw new NotFoundException(expression); + } + String value = result.toString(); + variables.put(name, value); + } + }); + } + + private void store(String expression, String name) { + if (expression.startsWith("javascript{") && expression.endsWith("}")) { + storeEval(expression.substring(11, expression.length() - 1), name); + } else { + throw new IllegalArgumentException("Invalid store expression: " + expression); + } + } + + private class ExtractVariable implements Consumer<WebElement> { + private final String name; + + ExtractVariable(String name) { + this.name = name; + } + + @Override + public void accept(WebElement webElement) { + variables.put(name, webElement.getText()); + } + + public String toString() { + return "read value into " + name; + } + } + + private void assertText(String selector, String pattern) { + pattern = replacePlaceholders(pattern); + + if (pattern.startsWith("exact:")) { + String expectedText = pattern.substring(6); + find(selector).withText(expectedText).should().exist(); + return; + } + + if (pattern.startsWith("regexp:")) { + find(selector).should().match(regex(pattern)); + return; + } + + find(selector).should().match(glob(pattern)); + } + + private void assertNotText(String selector, String pattern) { + pattern = replacePlaceholders(pattern); + + if (pattern.startsWith("exact:")) { + String expectedText = pattern.substring(6); + find(selector).withText(expectedText).should().not().exist(); + return; + } + + if (pattern.startsWith("regexp:")) { + find(selector).should().not().match(regex(pattern)); + return; + } + + find(selector).should().not().match(glob(pattern)); + } + + private static Pattern glob(String pattern) { + String regexp = pattern.replaceFirst("glob:", ""); + regexp = regexp.replaceAll("([\\]\\[\\\\{\\}$\\(\\)\\|\\^\\+.])", "\\\\$1"); + regexp = regexp.replaceAll("\\*", ".*"); + regexp = regexp.replaceAll("\\?", "."); + return Pattern.compile(regexp, DOTALL | Pattern.CASE_INSENSITIVE); + } + + private static Pattern regex(String pattern) { + String regexp = pattern.replaceFirst("regexp:", ".*") + ".*"; + return Pattern.compile(regexp, DOTALL | Pattern.CASE_INSENSITIVE); + } + + private void assertTextPresent(String text) { + find("body").should().contain(text); + } + + private void assertTextNotPresent(String text) { + find("body").should().not().contain(text); + } + + private void waitForElementPresent(String selector, String text) { + if (Strings.isNullOrEmpty(text)) { + find(selector).should().exist(); + } else { + find(selector).withText(text).should().exist(); + } + } + + private void waitForElementNotPresent(String selector, String text) { + if (Strings.isNullOrEmpty(text)) { + find(selector).should().not().exist(); + } else { + find(selector).withText(text).should().not().exist(); + } + } + + private void waitForVisible(String selector) { + find(selector).should().beDisplayed(); + } + + private void assertInputValue(String selector, String text) { + find(selector).should().contain(text); + } + + private void waitForXpathCount(String selector, int expectedCount) { + assertThat(find(selector).stream().size()).isEqualTo(expectedCount); + } + + private void confirm(final String message) { + System.out.println(" - confirm(" + message + ")"); + + _30_SECONDS.execute(new Runnable() { + @Override + public void run() { + driver.switchTo().alert().accept(); + } + }); + } + + private void assertLocation(String urlPattern) { + assertThat(driver.getCurrentUrl()).matches(glob(urlPattern)); + } + + private void verifyHtmlSource(String expect) { + assertThat(driver.getPageSource()).matches(glob(expect)); + } + + private String replacePlaceholders(String text) { + for (Map.Entry<String, String> entry : variables.entrySet()) { + text = text.replace("${" + entry.getKey() + "}", entry.getValue()); + } + return text; + } +} diff --git a/tests/src/test/java/util/selenium/Text.java b/tests/src/test/java/util/selenium/Text.java new file mode 100644 index 00000000000..5f3b8d259ce --- /dev/null +++ b/tests/src/test/java/util/selenium/Text.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import com.google.common.base.Joiner; +import org.openqa.selenium.By; + +public abstract class Text { + private Text() { + // Static utility class + } + + public static String doesOrNot(boolean not, String verb) { + if (!verb.contains(" ")) { + if (not) { + return "doesn't " + verb; + } else if (verb.endsWith("h")) { + return verb + "es"; + } else { + return verb + "s"; + } + } + + String[] verbs = verb.split(" "); + verbs[0] = doesOrNot(not, verbs[0]); + + return Joiner.on(" ").join(verbs); + } + + public static String isOrNot(boolean not, String state) { + return (not ? "is not " : "is ") + state; + } + + public static String plural(int n, String word) { + return (n + " " + word) + (n <= 1 ? "" : "s"); + } + + public static String toString(By selector) { + return selector.toString().replace("By.selector: ", "").replace("By.cssSelector: ", ""); + } +} diff --git a/tests/src/test/java/util/selenium/WebElementHelper.java b/tests/src/test/java/util/selenium/WebElementHelper.java new file mode 100644 index 00000000000..edb9d9dccb5 --- /dev/null +++ b/tests/src/test/java/util/selenium/WebElementHelper.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.selenium; + +import org.openqa.selenium.WebElement; + +class WebElementHelper { + WebElementHelper() { + // Static class + } + + public static String text(WebElement element) { + String text = element.getText(); + if (!"".equals(text)) { + return nullToEmpty(text); + } + + return nullToEmpty(element.getAttribute("value")); + } + + private static String nullToEmpty(String text) { + return (text == null) ? "" : text; + } +} diff --git a/tests/src/test/java/util/user/GroupManagement.java b/tests/src/test/java/util/user/GroupManagement.java new file mode 100644 index 00000000000..41b362f3e0a --- /dev/null +++ b/tests/src/test/java/util/user/GroupManagement.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.user; + +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; + +/** + * @deprecated replaced by {@link org.sonarqube.tests.Tester} + */ +@Deprecated +public interface GroupManagement { + void createGroup(String name); + + void createGroup(String name, @Nullable String description); + + void removeGroups(List<String> groupNames); + + void removeGroups(String... groupNames); + + Optional<Groups.Group> getGroupByName(String name); + + Groups getGroups(); + + void verifyUserGroupMembership(String userLogin, String... groups); + + Groups getUserGroups(String userLogin); + + void associateGroupsToUser(String userLogin, String... groups); +} diff --git a/tests/src/test/java/util/user/Groups.java b/tests/src/test/java/util/user/Groups.java new file mode 100644 index 00000000000..7b87547bc17 --- /dev/null +++ b/tests/src/test/java/util/user/Groups.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.user; + +import com.google.gson.Gson; +import java.util.List; + +/** + * @deprecated replaced by {@link org.sonarqube.tests.Tester} + */ +@Deprecated + +public class Groups { + + private List<Group> groups; + + private Groups(List<Group> groups) { + this.groups = groups; + } + + public List<Group> getGroups() { + return groups; + } + + public static Groups parse(String json) { + Gson gson = new Gson(); + return gson.fromJson(json, Groups.class); + } + + public static class Group { + private final String name; + + private Group(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/tests/src/test/java/util/user/UserRule.java b/tests/src/test/java/util/user/UserRule.java new file mode 100644 index 00000000000..fe93e631c01 --- /dev/null +++ b/tests/src/test/java/util/user/UserRule.java @@ -0,0 +1,395 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.user; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.FluentIterable; +import com.sonar.orchestrator.Orchestrator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.junit.rules.ExternalResource; +import org.sonarqube.ws.Organizations; +import org.sonarqube.ws.WsUsers; +import org.sonarqube.ws.client.GetRequest; +import org.sonarqube.ws.client.PostRequest; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.WsResponse; +import org.sonarqube.ws.client.permission.AddUserWsRequest; +import org.sonarqube.ws.client.user.CreateRequest; +import org.sonarqube.ws.client.user.SearchRequest; +import org.sonarqube.ws.client.user.UsersService; +import util.selenium.Consumer; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.guava.api.Assertions.assertThat; +import static util.ItUtils.newAdminWsClient; + +/** + * @deprecated replaced by {@link org.sonarqube.tests.Tester} + */ +@Deprecated +public class UserRule extends ExternalResource implements GroupManagement { + + public static final String ADMIN_LOGIN = "admin"; + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); + private final Orchestrator orchestrator; + + private WsClient adminWsClient; + private final GroupManagement defaultOrganizationGroupManagement; + + public UserRule(Orchestrator orchestrator) { + this.orchestrator = orchestrator; + this.defaultOrganizationGroupManagement = new GroupManagementImpl(null); + } + + public static UserRule from(Orchestrator orchestrator) { + return new UserRule(requireNonNull(orchestrator, "Orchestrator instance cannot be null")); + } + + @Override + protected void after() { + deactivateAllUsers(); + // TODO delete groups + } + + // ***************** + // Users + // ***************** + + public void resetUsers() { + for (Users.User user : getUsers().getUsers()) { + String userLogin = user.getLogin(); + if (!userLogin.equals(ADMIN_LOGIN)) { + deactivateUsers(userLogin); + } + } + } + + public Users.User verifyUserExists(String login, String name, @Nullable String email) { + Optional<Users.User> user = getUserByLogin(login); + assertThat(user).as("User with login '%s' hasn't been found", login).isPresent(); + assertThat(user.get().getLogin()).isEqualTo(login); + assertThat(user.get().getName()).isEqualTo(name); + assertThat(user.get().getEmail()).isEqualTo(email); + return user.get(); + } + + public void verifyUserExists(String login, String name, @Nullable String email, boolean local) { + Users.User user = verifyUserExists(login, name, email); + assertThat(user.isLocal()).isEqualTo(local); + } + + public void verifyUserDoesNotExist(String login) { + assertThat(getUserByLogin(login)).as("Unexpected user with login '%s' has been found", login).isAbsent(); + } + + public WsUsers.CreateWsResponse.User createUser(String login, String name, @Nullable String email, String password) { + CreateRequest.Builder request = CreateRequest.builder() + .setLogin(login) + .setName(name) + .setEmail(email) + .setPassword(password); + return adminWsClient().users().create(request.build()).getUser(); + } + + /** + * Create user with randomly generated values. By default password is the login. + */ + @SafeVarargs + public final WsUsers.CreateWsResponse.User generate(Consumer<CreateRequest.Builder>... populators) { + int id = ID_GENERATOR.getAndIncrement(); + String login = "login" + id; + CreateRequest.Builder request = CreateRequest.builder() + .setLogin(login) + .setName("name" + id) + .setEmail(id + "@test.com") + .setPassword(login); + stream(populators).forEach(p -> p.accept(request)); + return adminWsClient().users().create(request.build()).getUser(); + } + + public void createUser(String login, String password) { + createUser(login, login, null, password); + } + + public WsUsers.CreateWsResponse.User createAdministrator(Organizations.Organization organization, String password) { + WsUsers.CreateWsResponse.User user = generate(p -> p.setPassword(password)); + adminWsClient.organizations().addMember(organization.getKey(), user.getLogin()); + forOrganization(organization.getKey()).associateGroupsToUser(user.getLogin(), "Owners"); + return user; + } + + /** + * Create a new admin user with random login, having password same as login + */ + public String createAdminUser() { + String login = randomAlphabetic(10).toLowerCase(); + return createAdminUser(login, login); + } + + public String createAdminUser(String login, String password) { + createUser(login, password); + adminWsClient.permissions().addUser(new AddUserWsRequest().setLogin(login).setPermission("admin")); + adminWsClient.userGroups().addUser(org.sonarqube.ws.client.usergroup.AddUserWsRequest.builder().setLogin(login).setName("sonar-administrators").build()); + return login; + } + + /** + * Create a new root user with random login, having password same as login + */ + public String createRootUser() { + String login = randomAlphabetic(10).toLowerCase(); + return createRootUser(login, login); + } + + public String createRootUser(String login, String password) { + createUser(login, password); + setRoot(login); + return login; + } + + public void setRoot(String login) { + adminWsClient().roots().setRoot(login); + } + + public void unsetRoot(String login) { + adminWsClient().roots().unsetRoot(login); + } + + public Optional<Users.User> getUserByLogin(String login) { + return FluentIterable.from(getUsers().getUsers()).firstMatch(new MatchUserLogin(login)); + } + + public Users getUsers() { + WsResponse response = adminWsClient().wsConnector().call( + new GetRequest("api/users/search")) + .failIfNotSuccessful(); + return Users.parse(response.content()); + } + + public void deactivateUsers(List<String> userLogins) { + for (String userLogin : userLogins) { + if (getUserByLogin(userLogin).isPresent()) { + adminWsClient().wsConnector().call(new PostRequest("api/users/deactivate").setParam("login", userLogin)).failIfNotSuccessful(); + } + } + } + + public void deactivateUsers(String... userLogins) { + deactivateUsers(asList(userLogins)); + } + + public void deactivateAllUsers() { + UsersService service = newAdminWsClient(orchestrator).users(); + List<String> logins = service.search(SearchRequest.builder().build()).getUsersList() + .stream() + .filter(u -> !u.getLogin().equals("admin")) + .map(u -> u.getLogin()) + .collect(Collectors.toList()); + deactivateUsers(logins); + } + + // ***************** + // User groups + // ***************** + + public GroupManagement forOrganization(String organizationKey) { + return new GroupManagementImpl(organizationKey); + } + + private final class GroupManagementImpl implements GroupManagement { + @CheckForNull + private final String organizationKey; + + private GroupManagementImpl(@Nullable String organizationKey) { + this.organizationKey = organizationKey; + } + + @Override + public void createGroup(String name) { + createGroup(name, null); + } + + @Override + public void createGroup(String name, @Nullable String description) { + PostRequest request = new PostRequest("api/user_groups/create") + .setParam("name", name) + .setParam("description", description); + addOrganizationParam(request); + adminWsClient().wsConnector().call(request).failIfNotSuccessful(); + } + + private void addOrganizationParam(PostRequest request) { + request.setParam("organization", organizationKey); + } + + private void addOrganizationParam(GetRequest request) { + request.setParam("organization", organizationKey); + } + + @Override + public void removeGroups(List<String> groupNames) { + for (String groupName : groupNames) { + if (getGroupByName(groupName).isPresent()) { + PostRequest request = new PostRequest("api/user_groups/delete") + .setParam("name", groupName); + addOrganizationParam(request); + adminWsClient().wsConnector().call(request).failIfNotSuccessful(); + } + } + } + + @Override + public void removeGroups(String... groupNames) { + removeGroups(asList(groupNames)); + } + + @Override + public java.util.Optional<Groups.Group> getGroupByName(String name) { + return getGroups().getGroups().stream().filter(new MatchGroupName(name)::apply).findFirst(); + } + + @Override + public Groups getGroups() { + GetRequest request = new GetRequest("api/user_groups/search"); + addOrganizationParam(request); + WsResponse response = adminWsClient().wsConnector().call(request).failIfNotSuccessful(); + return Groups.parse(response.content()); + } + + @Override + public void verifyUserGroupMembership(String userLogin, String... expectedGroups) { + Groups userGroup = getUserGroups(userLogin); + List<String> userGroupName = userGroup.getGroups().stream().map(Groups.Group::getName).collect(Collectors.toList()); + assertThat(userGroupName).containsOnly(expectedGroups); + } + + @Override + public Groups getUserGroups(String userLogin) { + GetRequest request = new GetRequest("api/users/groups") + .setParam("login", userLogin) + .setParam("selected", "selected"); + addOrganizationParam(request); + WsResponse response = adminWsClient().wsConnector().call(request).failIfNotSuccessful(); + return Groups.parse(response.content()); + } + + @Override + public void associateGroupsToUser(String userLogin, String... groups) { + for (String group : groups) { + PostRequest request = new PostRequest("api/user_groups/add_user") + .setParam("login", userLogin) + .setParam("name", group); + addOrganizationParam(request); + adminWsClient().wsConnector().call(request).failIfNotSuccessful(); + } + } + } + + @Override + public void createGroup(String name) { + defaultOrganizationGroupManagement.createGroup(name); + } + + @Override + public void createGroup(String name, @Nullable String description) { + defaultOrganizationGroupManagement.createGroup(name, description); + } + + @Override + public void removeGroups(List<String> groupNames) { + defaultOrganizationGroupManagement.removeGroups(groupNames); + } + + @Override + public void removeGroups(String... groupNames) { + defaultOrganizationGroupManagement.removeGroups(groupNames); + } + + @Override + public java.util.Optional<Groups.Group> getGroupByName(String name) { + return defaultOrganizationGroupManagement.getGroupByName(name); + } + + @Override + public Groups getGroups() { + return defaultOrganizationGroupManagement.getGroups(); + } + + @Override + public void verifyUserGroupMembership(String userLogin, String... groups) { + defaultOrganizationGroupManagement.verifyUserGroupMembership(userLogin, groups); + } + + @Override + public Groups getUserGroups(String userLogin) { + return defaultOrganizationGroupManagement.getUserGroups(userLogin); + } + + @Override + public void associateGroupsToUser(String userLogin, String... groups) { + defaultOrganizationGroupManagement.associateGroupsToUser(userLogin, groups); + } + + private WsClient adminWsClient() { + if (adminWsClient == null) { + adminWsClient = newAdminWsClient(orchestrator); + } + return adminWsClient; + } + + private class MatchUserLogin implements Predicate<Users.User> { + private final String login; + + private MatchUserLogin(String login) { + this.login = login; + } + + @Override + public boolean apply(@Nonnull Users.User user) { + String login = user.getLogin(); + return login != null && login.equals(this.login) && user.isActive(); + } + } + + private class MatchGroupName implements Predicate<Groups.Group> { + private final String groupName; + + private MatchGroupName(String groupName) { + this.groupName = groupName; + } + + @Override + public boolean apply(@Nonnull Groups.Group group) { + String groupName = group.getName(); + return groupName != null && groupName.equals(this.groupName); + } + } + +} diff --git a/tests/src/test/java/util/user/Users.java b/tests/src/test/java/util/user/Users.java new file mode 100644 index 00000000000..2f0a252e6c7 --- /dev/null +++ b/tests/src/test/java/util/user/Users.java @@ -0,0 +1,108 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * 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 util.user; + +import com.google.gson.Gson; +import java.util.List; + +public class Users { + + private List<User> users; + + private Users(List<User> users) { + this.users = users; + } + + public List<User> getUsers() { + return users; + } + + public static Users parse(String json) { + Gson gson = new Gson(); + return gson.fromJson(json, Users.class); + } + + public static class User { + private final String login; + private final String name; + private final String email; + private final String externalIdentity; + private final String externalProvider; + private final List<String> groups; + private final List<String> scmAccounts; + private final boolean active; + private final boolean local; + private int tokensCount; + + private User(String login, String name, String email, String externalIdentity, String externalProvider, List<String> groups, List<String> scmAccounts, + boolean active, boolean local, int tokensCount) { + this.login = login; + this.name = name; + this.externalIdentity = externalIdentity; + this.externalProvider = externalProvider; + this.email = email; + this.groups = groups; + this.scmAccounts = scmAccounts; + this.active = active; + this.tokensCount = tokensCount; + this.local = local; + } + + public String getLogin() { + return login; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public List<String> getGroups() { + return groups; + } + + public List<String> getScmAccounts() { + return scmAccounts; + } + + public boolean isActive() { + return active; + } + + public boolean isLocal() { + return local; + } + + public int getTokensCount() { + return tokensCount; + } + + public String getExternalIdentity() { + return externalIdentity; + } + + public String getExternalProvider() { + return externalProvider; + } + } +} |