aboutsummaryrefslogtreecommitdiffstats
path: root/tests/src/test/java/util
diff options
context:
space:
mode:
Diffstat (limited to 'tests/src/test/java/util')
-rw-r--r--tests/src/test/java/util/ItUtils.java534
-rw-r--r--tests/src/test/java/util/LoadedProfiles.java87
-rw-r--r--tests/src/test/java/util/LoadedProjects.java82
-rw-r--r--tests/src/test/java/util/Profile.java49
-rw-r--r--tests/src/test/java/util/ProjectAnalysis.java51
-rw-r--r--tests/src/test/java/util/ProjectAnalysisRule.java220
-rw-r--r--tests/src/test/java/util/ProjectState.java71
-rw-r--r--tests/src/test/java/util/issue/IssueRule.java79
-rw-r--r--tests/src/test/java/util/selenium/ByCssSelectorOrByNameOrById.java107
-rw-r--r--tests/src/test/java/util/selenium/Consumer.java24
-rw-r--r--tests/src/test/java/util/selenium/ElementFilter.java68
-rw-r--r--tests/src/test/java/util/selenium/Failure.java49
-rw-r--r--tests/src/test/java/util/selenium/LazyDomElement.java174
-rw-r--r--tests/src/test/java/util/selenium/LazyShould.java190
-rw-r--r--tests/src/test/java/util/selenium/Optional.java57
-rw-r--r--tests/src/test/java/util/selenium/Retry.java152
-rw-r--r--tests/src/test/java/util/selenium/Selenese.java91
-rw-r--r--tests/src/test/java/util/selenium/SeleneseRunner.java440
-rw-r--r--tests/src/test/java/util/selenium/Text.java58
-rw-r--r--tests/src/test/java/util/selenium/WebElementHelper.java41
-rw-r--r--tests/src/test/java/util/user/GroupManagement.java48
-rw-r--r--tests/src/test/java/util/user/Groups.java58
-rw-r--r--tests/src/test/java/util/user/UserRule.java395
-rw-r--r--tests/src/test/java/util/user/Users.java108
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;
+ }
+ }
+}