]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12512 Migrate GitHub ALM settings to new tables
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 29 Oct 2019 14:14:54 +0000 (15:14 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 6 Nov 2019 09:04:30 +0000 (10:04 +0100)
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettings.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/DbVersion81Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettingsTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettingsTest/schema.sql [new file with mode: 0644]

index f946622d850762f62c94fc1dc3f0cb895e77a0ba..10a725d8a484d5c61f3c9c6b6233d525c6258f42 100644 (file)
@@ -27,6 +27,8 @@ public class DbVersion81 implements DbVersion {
   public void addSteps(MigrationStepRegistry registry) {
     registry
       .add(3100, "Create ALM_SETTINGS table", CreateAlmSettingsTable.class)
-      .add(3101, "Create PROJECT_ALM_SETTINGS table", CreateProjectAlmSettingsTable.class);
+      .add(3101, "Create PROJECT_ALM_SETTINGS table", CreateProjectAlmSettingsTable.class)
+      .add(3102, "Migrate GitHub ALM settings from PROPERTIES to ALM_SETTINGS tables", MigrateGithubAlmSettings.class)
+    ;
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettings.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettings.java
new file mode 100644 (file)
index 0000000..b3e64be
--- /dev/null
@@ -0,0 +1,335 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 org.sonar.server.platform.db.migration.version.v81;
+
+import com.google.common.collect.ImmutableSet;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+import org.sonar.server.platform.db.migration.step.Select;
+import org.sonar.server.platform.db.migration.step.SqlStatement;
+import org.sonar.server.platform.db.migration.step.Upsert;
+
+public class MigrateGithubAlmSettings extends DataChange {
+
+  private static final String PROVIDER = "sonar.pullrequest.provider";
+
+  // Global settings
+  private static final String GITHUB_ENDPOINT = "sonar.pullrequest.github.endpoint";
+  private static final String GITHUB_APP_ID = "sonar.alm.github.app.id";
+  private static final String GITHUB_APP_PRIVATE_KEY = "sonar.alm.github.app.privateKeyContent.secured";
+  private static final String GITHUB_APP_NAME = "sonar.alm.github.app.name";
+  // Project setting
+  private static final String GITHUB_REPOSITORY = "sonar.pullrequest.github.repository";
+
+  private static final Set<String> MANDATORY_GLOBAL_KEYS = ImmutableSet.of(GITHUB_ENDPOINT, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY);
+
+  private static final String PROVIDER_VALUE = "GitHub";
+
+  private static final String ALM_SETTING_GITHUB_ID = "github";
+
+  private static final String INSERT_SQL = "insert into project_alm_settings (uuid, project_uuid, alm_setting_uuid, alm_repo, created_at, updated_at) values (?, ?, ?, ?, ?, ?)";
+  private static final String DELETE_SQL = "delete from properties where prop_key = ? and resource_id = ?";
+
+  private final UuidFactory uuidFactory;
+  private final System2 system2;
+
+  public MigrateGithubAlmSettings(Database db, UuidFactory uuidFactory, System2 system2) {
+    super(db);
+    this.uuidFactory = uuidFactory;
+    this.system2 = system2;
+  }
+
+  @Override
+  protected void execute(Context context) throws SQLException {
+    Map<String, Property> globalPropertiesByKey = loadGlobalProperties(context);
+
+    if (globalPropertiesByKey.keySet().containsAll(MANDATORY_GLOBAL_KEYS)) {
+      String gitHubAlmSettingUuid = loadOrInsertAlmSetting(context, globalPropertiesByKey);
+      Property globalProviderProperty = globalPropertiesByKey.getOrDefault(PROVIDER, null);
+      String globalProvider = globalProviderProperty != null ? globalProviderProperty.getValue() : null;
+
+      insertProjectAlmSettings(context, globalProvider, gitHubAlmSettingUuid);
+    }
+
+    context.prepareUpsert("delete from properties where prop_key in (?, ?, ?, ?, ?)")
+      .setString(1, GITHUB_ENDPOINT)
+      .setString(2, GITHUB_APP_ID)
+      .setString(3, GITHUB_APP_PRIVATE_KEY)
+      .setString(4, GITHUB_APP_NAME)
+      .setString(5, GITHUB_REPOSITORY)
+      .execute()
+      .commit();
+  }
+
+  private static Map<String, Property> loadGlobalProperties(Context context) throws SQLException {
+    return context
+      .prepareSelect("select prop_key, text_value from properties where prop_key in (?, ?, ?, ?, ?) " +
+        "and resource_id is null " +
+        "and text_value is not null ")
+      .setString(1, PROVIDER)
+      .setString(2, GITHUB_ENDPOINT)
+      .setString(3, GITHUB_APP_ID)
+      .setString(4, GITHUB_APP_PRIVATE_KEY)
+      .setString(5, GITHUB_APP_NAME)
+      .list(Property::new)
+      .stream()
+      .collect(Collectors.toMap(Property::getKey, Function.identity()));
+  }
+
+  private String loadOrInsertAlmSetting(Context context, Map<String, Property> globalPropertiesByKey) throws SQLException {
+    String gitHubAlmSettingUuid = loadAlmSetting(context);
+    if (gitHubAlmSettingUuid != null) {
+      return gitHubAlmSettingUuid;
+    }
+    return insertAlmSetting(context, globalPropertiesByKey);
+  }
+
+  @CheckForNull
+  private static String loadAlmSetting(Context context) throws SQLException {
+    List<String> list = context.prepareSelect("select uuid from alm_settings where alm_id=?")
+      .setString(1, ALM_SETTING_GITHUB_ID)
+      .list(row -> row.getString(1));
+    if (list.isEmpty()) {
+      return null;
+    }
+    return list.get(0);
+  }
+
+  private String insertAlmSetting(Context context, Map<String, Property> globalPropertiesByKey) throws SQLException {
+    String gitHubAlmSettingUuid = uuidFactory.create();
+    Property appName = globalPropertiesByKey.get(GITHUB_APP_NAME);
+    context.prepareUpsert("insert into alm_settings (uuid, alm_id, kee, url, app_id, private_key, updated_at, created_at) values (?, ?, ?, ?, ?, ?, ?, ?)")
+      .setString(1, gitHubAlmSettingUuid)
+      .setString(2, ALM_SETTING_GITHUB_ID)
+      .setString(3, appName == null ? PROVIDER_VALUE : appName.getValue())
+      .setString(4, globalPropertiesByKey.get(GITHUB_ENDPOINT).getValue())
+      .setString(5, globalPropertiesByKey.get(GITHUB_APP_ID).getValue())
+      .setString(6, globalPropertiesByKey.get(GITHUB_APP_PRIVATE_KEY).getValue())
+      .setLong(7, system2.now())
+      .setLong(8, system2.now())
+      .execute()
+      .commit();
+    return gitHubAlmSettingUuid;
+  }
+
+  private void insertProjectAlmSettings(Context context, @Nullable String globalProvider, String almSettingUuid) throws SQLException {
+    final Buffer buffer = new Buffer(globalProvider);
+    MassUpdate massUpdate = context.prepareMassUpdate();
+    massUpdate.select("select prop_key, text_value, prj.uuid, prj.id from properties prop " +
+      "inner join projects prj on prj.id=prop.resource_id " +
+      "where prop.prop_key in (?, ?) " +
+      "order by prj.id asc")
+      .setString(1, PROVIDER)
+      .setString(2, GITHUB_REPOSITORY);
+    massUpdate.update(INSERT_SQL);
+    massUpdate.update(DELETE_SQL);
+    massUpdate.execute((row, update, updateIndex) -> {
+      boolean shouldExecuteUpdate = false;
+      String projectUuid = row.getString(3);
+      Long projectId = row.getLong(4);
+      // Set last projectUuid the first time
+      if (buffer.getLastProjectUuid() == null) {
+        buffer.setLastProject(projectUuid, projectId);
+      }
+      // When current projectUuid is different from last processed projectUuid, feed the prepared statement
+      if (!projectUuid.equals(buffer.getLastProjectUuid())) {
+        if (updateIndex == 0) {
+          // Insert new row in PROJECT_ALM_SETTINGS
+          shouldExecuteUpdate = updateStatementIfNeeded(buffer.getLastProjectUuid(), buffer, update, almSettingUuid);
+        } else {
+          // Delete old property
+          shouldExecuteUpdate = deleteProperty(buffer.getLastProjectId(), buffer, update);
+          buffer.clear();
+          // Update last projectUuid in buffer only when it has changed
+          buffer.setLastProject(projectUuid, projectId);
+        }
+      }
+      // Update remaining buffer values only once, and only after delete
+      if (updateIndex == 1) {
+        String propertyKey = row.getString(1);
+        String propertyValue = row.getString(2);
+        if (propertyKey.equals(PROVIDER)) {
+          buffer.setProvider(propertyValue);
+        } else if (propertyKey.equals(GITHUB_REPOSITORY)) {
+          buffer.setRepository(propertyValue);
+        }
+        buffer.setCurrentProject(projectUuid, projectId);
+      }
+      return shouldExecuteUpdate;
+    });
+    String projectUuid = buffer.getCurrentProjectUuid();
+    if (projectUuid == null) {
+      return;
+    }
+    // Process last entry
+    Upsert upsert = context.prepareUpsert(INSERT_SQL);
+    if (updateStatementIfNeeded(projectUuid, buffer, upsert, almSettingUuid)) {
+      upsert.execute().commit();
+    }
+    if (buffer.shouldDelete()) {
+      context.prepareUpsert(DELETE_SQL)
+        .setString(1, GITHUB_REPOSITORY)
+        .setLong(2, buffer.getCurrentProjectId())
+        .execute()
+        .commit();
+    }
+  }
+
+  private static boolean deleteProperty(long effectiveProjectId, Buffer buffer, SqlStatement update) throws SQLException {
+    if (buffer.shouldDelete()) {
+      update.setString(1, GITHUB_REPOSITORY);
+      update.setLong(2, effectiveProjectId);
+      return true;
+    }
+    return false;
+  }
+
+  private boolean updateStatementIfNeeded(String effectiveProjectUuid, Buffer buffer, SqlStatement update, String almSettingUuid)
+    throws SQLException {
+    if (!buffer.shouldUpdate()) {
+      return false;
+    }
+    update.setString(1, uuidFactory.create());
+    update.setString(2, effectiveProjectUuid);
+    update.setString(3, almSettingUuid);
+    update.setString(4, buffer.getRepository());
+    update.setLong(5, system2.now());
+    update.setLong(6, system2.now());
+    return true;
+  }
+
+  private static class Property {
+    private final String key;
+    private final String value;
+
+    Property(Select.Row row) throws SQLException {
+      this.key = row.getString(1);
+      this.value = row.getString(2);
+    }
+
+    String getKey() {
+      return key;
+    }
+
+    String getValue() {
+      return value;
+    }
+  }
+
+  private static class Buffer {
+    private final String globalProvider;
+    private String lastProjectUuid;
+    private String currentProjectUuid;
+    private Long lastProjectId;
+    private Long currentProjectId;
+    private String provider;
+    private String repository;
+
+    public Buffer(@Nullable String globalProvider) {
+      this.globalProvider = globalProvider;
+    }
+
+    Buffer setLastProject(@Nullable String projectUuid, @Nullable Long projectId) {
+      this.lastProjectUuid = projectUuid;
+      this.lastProjectId = projectId;
+      return this;
+    }
+
+    @CheckForNull
+    String getLastProjectUuid() {
+      return lastProjectUuid;
+    }
+
+    @CheckForNull
+    Long getLastProjectId() {
+      return lastProjectId;
+    }
+
+    Buffer setCurrentProject(@Nullable String projectUuid, @Nullable Long projectId) {
+      this.currentProjectUuid = projectUuid;
+      this.currentProjectId = projectId;
+      return this;
+    }
+
+    @CheckForNull
+    String getCurrentProjectUuid() {
+      return currentProjectUuid;
+    }
+
+    @CheckForNull
+    Long getCurrentProjectId() {
+      return currentProjectId;
+    }
+
+    Buffer setProvider(@Nullable String provider) {
+      this.provider = provider;
+      return this;
+    }
+
+    @CheckForNull
+    String getRepository() {
+      return repository;
+    }
+
+    Buffer setRepository(@Nullable String repository) {
+      this.repository = repository;
+      return this;
+    }
+
+    boolean shouldUpdate() {
+      if (repository == null) {
+        return false;
+      }
+      if (Objects.equals(provider, PROVIDER_VALUE)) {
+        return true;
+      }
+      return provider == null && Objects.equals(globalProvider, PROVIDER_VALUE);
+    }
+
+    boolean shouldDelete() {
+      if (provider != null) {
+        return provider.equals(PROVIDER_VALUE);
+      }
+      return Objects.equals(globalProvider, PROVIDER_VALUE);
+    }
+
+    void clear() {
+      this.lastProjectUuid = null;
+      this.currentProjectUuid = null;
+      this.lastProjectId = null;
+      this.currentProjectId = null;
+      this.provider = null;
+      this.repository = null;
+    }
+  }
+
+}
index 8e6e0fa4f1e668c3c35879543741e0425b668f1c..7c9ac08216fd1f16607fcfeea8414c636ab57ec5 100644 (file)
@@ -37,7 +37,7 @@ public class DbVersion81Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 2);
+    verifyMigrationCount(underTest, 3);
   }
 
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettingsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettingsTest.java
new file mode 100644 (file)
index 0000000..6bcc05a
--- /dev/null
@@ -0,0 +1,309 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 org.sonar.server.platform.db.migration.version.v81;
+
+import java.sql.SQLException;
+import javax.annotation.Nullable;
+import org.assertj.core.groups.Tuple;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.commons.lang.math.RandomUtils.nextInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class MigrateGithubAlmSettingsTest {
+
+  private final static long PAST = 10_000_000_000L;
+  private static final long NOW = 50_000_000_000L;
+  private System2 system2 = new TestSystem2().setNow(NOW);
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(MigrateGithubAlmSettingsTest.class, "schema.sql");
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private UuidFactory uuidFactory = UuidFactoryFast.getInstance();
+
+  private DataChange underTest = new MigrateGithubAlmSettings(db.database(), uuidFactory, system2);
+
+  @Test
+  public void migrate_settings_when_global_provider_is_set_to_github() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    insertProperty("sonar.pullrequest.github.repository", "Repository1", projectId1);
+    long projectId2 = insertProject("PROJECT_2");
+    insertProperty("sonar.pullrequest.github.repository", "Repository2", projectId2);
+
+    underTest.execute();
+
+    assertAlmSettings(tuple("github", "GitHub", "https://enterprise.github.com", "12345", "<PRIVATE_KEY>", NOW, NOW));
+    String gitHubAlmSettingUuid = selectAlmSettingUuid("GitHub");
+    assertProjectAlmSettings(
+      tuple("PROJECT_1", gitHubAlmSettingUuid, "Repository1", NOW, NOW),
+      tuple("PROJECT_2", gitHubAlmSettingUuid, "Repository2", NOW, NOW));
+    assertProperties(tuple("sonar.pullrequest.provider", "GitHub", null));
+  }
+
+  @Test
+  public void migrate_settings_when_project_provider_is_set_to_github_and_global_provider_is_set_to_something_else() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "Azure DevOps", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    insertProperty("sonar.pullrequest.provider", "GitHub", projectId1);
+    insertProperty("sonar.pullrequest.github.repository", "Repository1", projectId1);
+    long projectId2 = insertProject("PROJECT_2");
+    insertProperty("sonar.pullrequest.provider", "GitHub", projectId2);
+    insertProperty("sonar.pullrequest.github.repository", "Repository2", projectId2);
+
+    underTest.execute();
+
+    assertAlmSettings(tuple("github", "GitHub", "https://enterprise.github.com", "12345", "<PRIVATE_KEY>", NOW, NOW));
+    String gitHubAlmSettingUuid = selectAlmSettingUuid("GitHub");
+    assertProjectAlmSettings(
+      tuple("PROJECT_1", gitHubAlmSettingUuid, "Repository1", NOW, NOW),
+      tuple("PROJECT_2", gitHubAlmSettingUuid, "Repository2", NOW, NOW));
+    assertProperties(
+      tuple("sonar.pullrequest.provider", "Azure DevOps", null),
+      tuple("sonar.pullrequest.provider", "GitHub", projectId1),
+      tuple("sonar.pullrequest.provider", "GitHub", projectId2));
+  }
+
+  @Test
+  public void delete_github_settings_when_project_provider_is_not_set_to_github() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    // Project provider is set to something else
+    insertProperty("sonar.pullrequest.provider", "Azure", projectId1);
+    insertProperty("sonar.pullrequest.github.repository", "Repository1", projectId1);
+
+    underTest.execute();
+
+    assertNoProjectAlmSettings();
+    assertProperties(
+      tuple("sonar.pullrequest.provider", "GitHub", null),
+      tuple("sonar.pullrequest.provider", "Azure", projectId1));
+  }
+
+  @Test
+  public void use_existing_alm_setting() throws SQLException {
+    db.executeInsert("alm_settings",
+      "uuid", "ABCD",
+      "alm_id", "github",
+      "kee", "GitHub",
+      "url", "https://enterprise.github.com",
+      "app_id", "12345",
+      "private_key", "<PRIVATE_KEY>",
+      "created_at", PAST,
+      "updated_at", PAST);
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    insertProperty("sonar.pullrequest.github.repository", "Repository1", projectId1);
+
+    underTest.execute();
+
+    assertProjectAlmSettings(tuple("PROJECT_1", "ABCD", "Repository1", NOW, NOW));
+    assertProperties(tuple("sonar.pullrequest.provider", "GitHub", null));
+  }
+
+  @Test
+  public void ignore_none_github_project_settings() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    insertProperty("sonar.pullrequest.provider", "Bitbucket", projectId1);
+    long projectId2 = insertProject("PROJECT_2");
+    insertProperty("sonar.pullrequest.provider", "Bitbucket", projectId2);
+
+    underTest.execute();
+
+    assertAlmSettings(tuple("github", "GitHub", "https://enterprise.github.com", "12345", "<PRIVATE_KEY>", NOW, NOW));
+    assertNoProjectAlmSettings();
+    assertProperties(
+      tuple("sonar.pullrequest.provider", "GitHub", null),
+      tuple("sonar.pullrequest.provider", "Bitbucket", projectId1),
+      tuple("sonar.pullrequest.provider", "Bitbucket", projectId2));
+  }
+
+  @Test
+  public void do_not_create_alm_settings_when_missing_some_global_properties() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    // No endpoint
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    insertProperty("sonar.pullrequest.github.repository", "Repository1", projectId1);
+
+    underTest.execute();
+
+    assertNoAlmSettings();
+    assertNoProjectAlmSettings();
+    assertProperties(tuple("sonar.pullrequest.provider", "GitHub", null));
+  }
+
+  @Test
+  public void do_not_create_project_alm_settings_when_missing_some_project_properties() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    // No repository
+    insertProperty("sonar.pullrequest.provider", "GitHub", projectId1);
+
+    underTest.execute();
+
+    assertAlmSettings(tuple("github", "GitHub", "https://enterprise.github.com", "12345", "<PRIVATE_KEY>", NOW, NOW));
+    assertNoProjectAlmSettings();
+    assertProperties(
+      tuple("sonar.pullrequest.provider", "GitHub", null),
+      tuple("sonar.pullrequest.provider", "GitHub", projectId1));
+  }
+
+  @Test
+  public void do_nothing_when_no_alm_properties() throws SQLException {
+    insertProperty("sonar.other.property", "Something", null);
+
+    underTest.execute();
+
+    assertNoAlmSettings();
+    assertNoProjectAlmSettings();
+    assertProperties(tuple("sonar.other.property", "Something", null));
+  }
+
+  @Test
+  public void migration_is_reentrant() throws SQLException {
+    insertProperty("sonar.pullrequest.provider", "GitHub", null);
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId1 = insertProject("PROJECT_1");
+    insertProperty("sonar.pullrequest.provider", "GitHub", projectId1);
+    insertProperty("sonar.pullrequest.github.repository", "Repository1", projectId1);
+    underTest.execute();
+
+    // Global settings have been removed, let's re-create them
+    insertProperty("sonar.pullrequest.github.endpoint", "https://enterprise.github.com", null);
+    insertProperty("sonar.alm.github.app.id", "12345", null);
+    insertProperty("sonar.alm.github.app.privateKeyContent.secured", "<PRIVATE_KEY>", null);
+    long projectId2 = insertProject("PROJECT_2");
+    insertProperty("sonar.pullrequest.github.repository", "Repository2", projectId2);
+    underTest.execute();
+
+    assertAlmSettings(tuple("github", "GitHub", "https://enterprise.github.com", "12345", "<PRIVATE_KEY>", NOW, NOW));
+    assertProperties(
+      tuple("sonar.pullrequest.provider", "GitHub", null),
+      tuple("sonar.pullrequest.provider", "GitHub", projectId1));
+  }
+
+  private void assertAlmSettings(Tuple... expectedTuples) {
+    assertThat(db.select("SELECT alm_id, kee, url, app_id, private_key, created_at, updated_at FROM alm_settings")
+      .stream()
+      .map(map -> new Tuple(map.get("ALM_ID"), map.get("KEE"), map.get("URL"), map.get("APP_ID"), map.get("PRIVATE_KEY"), map.get("CREATED_AT"),
+        map.get("UPDATED_AT")))
+      .collect(toList()))
+        .containsExactlyInAnyOrder(expectedTuples);
+  }
+
+  private void assertNoAlmSettings() {
+    assertAlmSettings();
+  }
+
+  private void assertProjectAlmSettings(Tuple... expectedTuples) {
+    assertThat(db.select("SELECT project_uuid, alm_setting_uuid, alm_repo, created_at, updated_at FROM project_alm_settings")
+      .stream()
+      .map(map -> new Tuple(map.get("PROJECT_UUID"), map.get("ALM_SETTING_UUID"), map.get("ALM_REPO"), map.get("CREATED_AT"), map.get("UPDATED_AT")))
+      .collect(toList()))
+        .containsExactlyInAnyOrder(expectedTuples);
+  }
+
+  private void assertNoProjectAlmSettings() {
+    assertProjectAlmSettings();
+  }
+
+  private void assertProperties(Tuple... expectedTuples) {
+    assertThat(db.select("SELECT prop_key, text_value, resource_id FROM properties")
+      .stream()
+      .map(map -> new Tuple(map.get("PROP_KEY"), map.get("TEXT_VALUE"), map.get("RESOURCE_ID")))
+      .collect(toSet()))
+        .containsExactlyInAnyOrder(expectedTuples);
+  }
+
+  private void assertNoProperties() {
+    assertProperties();
+  }
+
+  private String selectAlmSettingUuid(String almSettingKey) {
+    return (String) db.selectFirst("select uuid from alm_settings where kee='" + almSettingKey + "'").get("UUID");
+  }
+
+  private void insertProperty(String key, String value, @Nullable Long projectId) {
+    db.executeInsert(
+      "PROPERTIES",
+      "PROP_KEY", key,
+      "RESOURCE_ID", projectId,
+      "USER_ID", null,
+      "IS_EMPTY", false,
+      "TEXT_VALUE", value,
+      "CLOB_VALUE", null,
+      "CREATED_AT", System2.INSTANCE.now());
+  }
+
+  private long insertProject(String uuid) {
+    int id = nextInt();
+    db.executeInsert("PROJECTS",
+      "ID", id,
+      "ORGANIZATION_UUID", "default",
+      "KEE", uuid + "-key",
+      "UUID", uuid,
+      "PROJECT_UUID", uuid,
+      "MAIN_BRANCH_PROJECT_UUID", uuid,
+      "UUID_PATH", ".",
+      "ROOT_UUID", uuid,
+      "PRIVATE", Boolean.toString(false),
+      "SCOPE", "PRJ",
+      "QUALIFIER", "PRJ");
+    return id;
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettingsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v81/MigrateGithubAlmSettingsTest/schema.sql
new file mode 100644 (file)
index 0000000..7fe0d58
--- /dev/null
@@ -0,0 +1,86 @@
+CREATE TABLE ALM_SETTINGS(
+    UUID VARCHAR(40) NOT NULL,
+    ALM_ID VARCHAR(40) NOT NULL,
+    KEE VARCHAR(200) NOT NULL,
+    URL VARCHAR(2000),
+    APP_ID VARCHAR(80),
+    PRIVATE_KEY VARCHAR(2000),
+    PAT VARCHAR(2000),
+    UPDATED_AT BIGINT NOT NULL,
+    CREATED_AT BIGINT NOT NULL
+);
+ALTER TABLE ALM_SETTINGS ADD CONSTRAINT PK_ALM_SETTINGS PRIMARY KEY(UUID);
+CREATE UNIQUE INDEX UNIQ_ALM_SETTINGS ON ALM_SETTINGS(KEE);
+
+CREATE TABLE PROJECT_ALM_SETTINGS(
+    UUID VARCHAR(40) NOT NULL,
+    ALM_SETTING_UUID VARCHAR(40) NOT NULL,
+    PROJECT_UUID VARCHAR(50) NOT NULL,
+    ALM_REPO VARCHAR(256),
+    ALM_SLUG VARCHAR(256),
+    UPDATED_AT BIGINT NOT NULL,
+    CREATED_AT BIGINT NOT NULL
+);
+ALTER TABLE PROJECT_ALM_SETTINGS ADD CONSTRAINT PK_PROJECT_ALM_SETTINGS PRIMARY KEY(UUID);
+CREATE UNIQUE INDEX UNIQ_PROJECT_ALM_SETTINGS ON PROJECT_ALM_SETTINGS(PROJECT_UUID);
+CREATE INDEX PROJECT_ALM_SETTINGS_ALM ON PROJECT_ALM_SETTINGS(ALM_SETTING_UUID);
+
+CREATE TABLE "PROPERTIES" (
+  "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1),
+  "PROP_KEY" VARCHAR(512) NOT NULL,
+  "RESOURCE_ID" INTEGER,
+  "USER_ID" INTEGER,
+  "IS_EMPTY" BOOLEAN NOT NULL,
+  "TEXT_VALUE" VARCHAR(4000),
+  "CLOB_VALUE" CLOB,
+  "CREATED_AT" BIGINT
+);
+CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES" ("PROP_KEY");
+
+CREATE TABLE PROJECTS(
+    ID INTEGER NOT NULL AUTO_INCREMENT (1,1),
+    UUID VARCHAR(50) NOT NULL,
+    ORGANIZATION_UUID VARCHAR(40) NOT NULL,
+    KEE VARCHAR(400),
+    DEPRECATED_KEE VARCHAR(400),
+    NAME VARCHAR(2000),
+    LONG_NAME VARCHAR(2000),
+    DESCRIPTION VARCHAR(2000),
+    ENABLED BOOLEAN DEFAULT TRUE NOT NULL,
+    SCOPE VARCHAR(3),
+    QUALIFIER VARCHAR(10),
+    PRIVATE BOOLEAN NOT NULL,
+    ROOT_UUID VARCHAR(50) NOT NULL,
+    LANGUAGE VARCHAR(20),
+    COPY_COMPONENT_UUID VARCHAR(50),
+    DEVELOPER_UUID VARCHAR(50),
+    PATH VARCHAR(2000),
+    UUID_PATH VARCHAR(1500) NOT NULL,
+    PROJECT_UUID VARCHAR(50) NOT NULL,
+    MODULE_UUID VARCHAR(50),
+    MODULE_UUID_PATH VARCHAR(1500),
+    AUTHORIZATION_UPDATED_AT BIGINT,
+    TAGS VARCHAR(500),
+    MAIN_BRANCH_PROJECT_UUID VARCHAR(50),
+    B_CHANGED BOOLEAN,
+    B_NAME VARCHAR(500),
+    B_LONG_NAME VARCHAR(500),
+    B_DESCRIPTION VARCHAR(2000),
+    B_ENABLED BOOLEAN,
+    B_QUALIFIER VARCHAR(10),
+    B_LANGUAGE VARCHAR(20),
+    B_COPY_COMPONENT_UUID VARCHAR(50),
+    B_PATH VARCHAR(2000),
+    B_UUID_PATH VARCHAR(1500),
+    B_MODULE_UUID VARCHAR(50),
+    B_MODULE_UUID_PATH VARCHAR(1500),
+    CREATED_AT TIMESTAMP
+);
+ALTER TABLE PROJECTS ADD CONSTRAINT PK_PROJECTS PRIMARY KEY(ID);
+CREATE INDEX PROJECTS_ORGANIZATION ON PROJECTS(ORGANIZATION_UUID);
+CREATE UNIQUE INDEX PROJECTS_KEE ON PROJECTS(KEE);
+CREATE INDEX PROJECTS_MODULE_UUID ON PROJECTS(MODULE_UUID);
+CREATE INDEX PROJECTS_PROJECT_UUID ON PROJECTS(PROJECT_UUID);
+CREATE INDEX PROJECTS_QUALIFIER ON PROJECTS(QUALIFIER);
+CREATE INDEX PROJECTS_ROOT_UUID ON PROJECTS(ROOT_UUID);
+CREATE INDEX PROJECTS_UUID ON PROJECTS(UUID);