]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10346 fix DB migration 3148/head
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 12 Mar 2018 14:49:55 +0000 (15:49 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 15 Mar 2018 11:08:14 +0000 (12:08 +0100)
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql

index fb782d5f236e84a7a7fc467186ad67812d329cba..f7894893cca25d6d92c06ab0d9c452d81a865987 100644 (file)
  */
 package org.sonar.server.platform.db.migration.version.v71;
 
+import com.google.common.collect.Multimap;
 import java.sql.SQLException;
-import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.function.Function;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.Database;
 import org.sonar.server.platform.db.migration.step.DataChange;
 import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUuidProvider;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toMap;
 
 public class MigrateWebhooksToWebhooksTable extends DataChange {
 
+  private static final long NO_RESOURCE_ID = -8_435_121;
   private static final Logger LOGGER = Loggers.get(MigrateWebhooksToWebhooksTable.class);
 
   private DefaultOrganizationUuidProvider defaultOrganizationUuidProvider;
@@ -53,47 +56,113 @@ public class MigrateWebhooksToWebhooksTable extends DataChange {
 
   @Override
   public void execute(Context context) throws SQLException {
-    Map<String, PropertyRow> rows = context
-      .prepareSelect("select id, prop_key, resource_id, text_value, created_at from properties where prop_key like 'sonar.webhooks%'")
+    Multimap<Long, PropertyRow> rows = context
+      .prepareSelect("select" +
+        " props.id, props.prop_key, props.resource_id, prj.uuid, props.text_value, props.created_at" +
+        " from properties props" +
+        " left join projects prj on prj.id = props.resource_id and prj.scope = ? and prj.qualifier = ? and prj.enabled = ?" +
+        " where" +
+        " props.prop_key like 'sonar.webhooks%'" +
+        " and props.text_value is not null")
+      .setString(1, "PRJ")
+      .setString(2, "TRK")
+      .setBoolean(3, true)
       .list(row -> new PropertyRow(
         row.getLong(1),
         row.getString(2),
-        row.getLong(3),
-        row.getString(4),
-        row.getLong(5)))
+        row.getNullableLong(3),
+        row.getNullableString(4),
+        row.getString(5),
+        row.getLong(6)))
       .stream()
-      .collect(toMap(PropertyRow::key, Function.identity()));
-
-    if (!rows.isEmpty()) {
-      migrateGlobalWebhooks(context, rows);
-      migrateProjectsWebhooks(context, rows);
-      context
-        .prepareUpsert("delete from properties where prop_key like 'sonar.webhooks.global%' or prop_key like 'sonar.webhooks.project%'")
-        .execute()
-        .commit();
+      .collect(MoreCollectors.index(PropertyRow::getResourceId, Function.identity()));
+
+    for (Map.Entry<Long, Collection<PropertyRow>> entry : rows.asMap().entrySet()) {
+      long projectId = entry.getKey();
+      if (projectId == NO_RESOURCE_ID) {
+        migrateGlobalWebhooks(context, entry.getValue());
+      } else {
+        migrateProjectsWebhooks(context, entry.getValue());
+      }
+      deleteAllWebhookProperties(context);
     }
   }
 
-  private void migrateProjectsWebhooks(Context context, Map<String, PropertyRow> properties) throws SQLException {
-    PropertyRow index = properties.get("sonar.webhooks.project");
-    if (index != null) {
+  private static void deleteAllWebhookProperties(Context context) throws SQLException {
+    context
+      .prepareUpsert("delete from properties where prop_key like 'sonar.webhooks.global%' or prop_key like 'sonar.webhooks.project%'")
+      .execute()
+      .commit();
+  }
+
+  private void migrateGlobalWebhooks(Context context, Collection<PropertyRow> rows) throws SQLException {
+    Multimap<String, PropertyRow> rowsByPropertyKey = rows.stream()
+      .collect(MoreCollectors.index(PropertyRow::getPropertyKey));
+    Optional<PropertyRow> rootProperty = rowsByPropertyKey.get("sonar.webhooks.global").stream().findFirst();
+    if (rootProperty.isPresent()) {
+      PropertyRow row = rootProperty.get();
       // can't lambda due to checked exception.
-      for (Webhook webhook : extractProjectWebhooksFrom(context, properties, index.value().split(","))) {
+      for (Webhook webhook : extractGlobalWebhooksFrom(context, rowsByPropertyKey, row.value().split(","))) {
         insert(context, webhook);
       }
     }
   }
 
-  private void migrateGlobalWebhooks(Context context, Map<String, PropertyRow> properties) throws SQLException {
-    PropertyRow index = properties.get("sonar.webhooks.global");
-    if (index != null) {
-      // can't lambda due to checked exception.
-      for (Webhook webhook : extractGlobalWebhooksFrom(context, properties, index.value().split(","))) {
-        insert(context, webhook);
+  private List<Webhook> extractGlobalWebhooksFrom(Context context, Multimap<String, PropertyRow> rowsByPropertyKey, String[] values) throws SQLException {
+    String defaultOrganizationUuid = defaultOrganizationUuidProvider.get(context);
+    return Arrays.stream(values)
+      .map(value -> {
+        Optional<PropertyRow> name = rowsByPropertyKey.get("sonar.webhooks.global." + value + ".name").stream().findFirst();
+        Optional<PropertyRow> url = rowsByPropertyKey.get("sonar.webhooks.global." + value + ".url").stream().findFirst();
+        if (name.isPresent() && url.isPresent()) {
+          return new Webhook(
+            name.get(),
+            url.get(),
+            defaultOrganizationUuid,
+            null);
+        }
+        LOGGER.warn(
+          "Global webhook missing name and/or url will be deleted (name='{}', url='{}')",
+          name.map(PropertyRow::value).orElse(null),
+          url.map(PropertyRow::value).orElse(null));
+        return null;
+      })
+      .filter(Objects::nonNull)
+      .collect(toList());
+  }
+
+  private void migrateProjectsWebhooks(Context context, Collection<PropertyRow> rows) throws SQLException {
+    Multimap<String, PropertyRow> rowsByPropertyKey = rows.stream()
+      .collect(MoreCollectors.index(PropertyRow::getPropertyKey));
+    Optional<PropertyRow> rootProperty = rowsByPropertyKey.get("sonar.webhooks.project").stream().findFirst();
+    if (rootProperty.isPresent()) {
+      PropertyRow row = rootProperty.get();
+      if (row.getProjectUuid() == null) {
+        LOGGER.warn("At least one webhook referenced missing or non project resource '{}' and will be deleted", row.getResourceId());
+      } else {
+        for (Webhook webhook : extractProjectWebhooksFrom(row, rowsByPropertyKey, row.value().split(","))) {
+          insert(context, webhook);
+        }
       }
     }
   }
 
+  private static List<Webhook> extractProjectWebhooksFrom(PropertyRow row, Multimap<String, PropertyRow> properties, String[] values) {
+    return Arrays.stream(values)
+      .map(value -> {
+        Optional<PropertyRow> name = properties.get("sonar.webhooks.project." + value + ".name").stream().findFirst();
+        Optional<PropertyRow> url = properties.get("sonar.webhooks.project." + value + ".url").stream().findFirst();
+        if (name.isPresent() && url.isPresent()) {
+          return new Webhook(name.get(), url.get(), null, row.projectUuid);
+        }
+        LOGGER.warn("Project webhook for project {} (id={}) missing name and/or url will be deleted (name='{}', url='{}')",
+          row.getProjectUuid(), row.getResourceId(), name.map(PropertyRow::value).orElse(null), url.map(PropertyRow::value).orElse(null));
+        return null;
+      })
+      .filter(Objects::nonNull)
+      .collect(MoreCollectors.toList());
+  }
+
   private void insert(Context context, Webhook webhook) throws SQLException {
     if (webhook.isValid()) {
       context.prepareUpsert("insert into webhooks (uuid, name, url, organization_uuid, project_uuid, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)")
@@ -111,47 +180,19 @@ public class MigrateWebhooksToWebhooksTable extends DataChange {
     }
   }
 
-  private List<Webhook> extractGlobalWebhooksFrom(Context context, Map<String, PropertyRow> properties, String[] values) throws SQLException {
-    String defaultOrganizationUuid = defaultOrganizationUuidProvider.get(context);
-    return Arrays.stream(values)
-      .map(value -> new Webhook(
-        properties.get("sonar.webhooks.global." + value + ".name"),
-        properties.get("sonar.webhooks.global." + value + ".url"),
-        defaultOrganizationUuid, null))
-      .collect(toList());
-  }
-
-  private static List<Webhook> extractProjectWebhooksFrom(Context context, Map<String, PropertyRow> properties, String[] values) throws SQLException {
-    List<Webhook> webhooks = new ArrayList<>();
-    for (String value : values) {
-      PropertyRow name = properties.get("sonar.webhooks.project." + value + ".name");
-      PropertyRow url = properties.get("sonar.webhooks.project." + value + ".url");
-      String projectUuid = checkNotNull(projectUuidOf(context, name), "Project was not found for property : sonar.webhooks.project.%s", value);
-      webhooks.add(new Webhook(name, url, null, projectUuid));
-    }
-    return webhooks;
-  }
-
-  @CheckForNull
-  private static String projectUuidOf(Context context, PropertyRow row) throws SQLException {
-    return context
-      .prepareSelect("select uuid from projects where id = ?")
-      .setLong(1, row.resourceId())
-      .list(row1 -> row1.getString(1)).stream().findFirst().orElse(null);
-  }
-
   private static class PropertyRow {
-
     private final Long id;
-    private final String key;
+    private final String propertyKey;
     private final Long resourceId;
+    private final String projectUuid;
     private final String value;
     private final Long createdAt;
 
-    public PropertyRow(long id, String key, Long resourceId, String value, Long createdAt) {
+    private PropertyRow(long id, String propertyKey, @Nullable Long resourceId, @Nullable String projectUuid, String value, Long createdAt) {
       this.id = id;
-      this.key = key;
+      this.propertyKey = propertyKey;
       this.resourceId = resourceId;
+      this.projectUuid = projectUuid;
       this.value = value;
       this.createdAt = createdAt;
     }
@@ -160,12 +201,17 @@ public class MigrateWebhooksToWebhooksTable extends DataChange {
       return id;
     }
 
-    public String key() {
-      return key;
+    public String getPropertyKey() {
+      return propertyKey;
     }
 
-    public Long resourceId() {
-      return resourceId;
+    public long getResourceId() {
+      return resourceId == null ? NO_RESOURCE_ID : resourceId;
+    }
+
+    @CheckForNull
+    public String getProjectUuid() {
+      return projectUuid;
     }
 
     public String value() {
@@ -180,8 +226,9 @@ public class MigrateWebhooksToWebhooksTable extends DataChange {
     public String toString() {
       return "{" +
         "id=" + id +
-        ", key='" + key + '\'' +
+        ", propertyKey='" + propertyKey + '\'' +
         ", resourceId=" + resourceId +
+        ", projectUuid=" + projectUuid +
         ", value='" + value + '\'' +
         ", createdAt=" + createdAt +
         '}';
index 6145fa74e0fa19b7ac43f4391ef6b05d851a1a1a..1aea7b3aeef09e22e02ca560150cbe7e235f8362 100644 (file)
  */
 package org.sonar.server.platform.db.migration.version.v71;
 
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.sql.SQLException;
+import java.util.Arrays;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
-import org.sonar.api.utils.System2;
-import org.sonar.api.utils.internal.TestSystem2;
+import org.junit.runner.RunWith;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.resources.Scopes;
 import org.sonar.core.util.UuidFactory;
 import org.sonar.core.util.UuidFactoryFast;
 import org.sonar.db.CoreDbTester;
@@ -33,146 +43,220 @@ import org.sonar.server.platform.db.migration.version.v63.DefaultOrganizationUui
 
 import static java.lang.Long.parseLong;
 import static java.lang.String.valueOf;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.apache.commons.lang.RandomStringUtils.randomNumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 
+@RunWith(DataProviderRunner.class)
 public class MigrateWebhooksToWebhooksTableTest {
+  private static final long NOW = 1_500_000_000_000L;
+  private static final boolean ENABLED = true;
+  private static final boolean DISABLED = false;
 
   @Rule
   public final CoreDbTester dbTester = CoreDbTester.createForSchema(MigrateWebhooksToWebhooksTableTest.class, "migrate_webhooks.sql");
-  private static final long NOW = 1_500_000_000_000L;
-  private System2 system2 = new TestSystem2().setNow(NOW);
+
   private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
 
   private MigrateWebhooksToWebhooksTable underTest = new MigrateWebhooksToWebhooksTable(dbTester.database(), new DefaultOrganizationUuidProviderImpl(), uuidFactory);
 
   @Test
   public void should_do_nothing_if_no_webhooks() throws SQLException {
-
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
+    assertNoMoreWebhookProperties();
     assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0);
   }
 
   @Test
-  public void should_migrate_one_global_webhook() throws SQLException {
-    String uuid = insertDefaultOrganization();
-    insertProperty("sonar.webhooks.global", "1", null, system2.now());
-    insertProperty("sonar.webhooks.global.1.name", "a webhook", null, system2.now());
-    insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, system2.now());
+  @UseDataProvider("numberOfGlobalWebhooksToMigration")
+  public void execute_migrates_any_number_of_global_webhook_to_default_organization(int numberOfGlobalWebhooks) throws SQLException {
+    String defaultOrganizationUuid = insertDefaultOrganization();
+    insertGlobalWebhookProperties(numberOfGlobalWebhooks);
+    Row[] webhooks = IntStream.range(1, numberOfGlobalWebhooks + 1)
+      .mapToObj(i -> insertGlobalWebhookProperty(i, "name webhook " + i, "url webhook " + i, defaultOrganizationUuid))
+      .map(Row::new)
+      .toArray(Row[]::new);
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1);
+    assertThat(selectWebhooksInDb())
+      .containsOnly(webhooks)
+      .extracting(Row::getUuid)
+      .doesNotContainNull();
+    assertNoMoreWebhookProperties();
+  }
 
-    Map<String, Object> migrated = dbTester.selectFirst("select * from webhooks");
-    assertThat(migrated.get("UUID")).isNotNull();
-    assertThat(migrated.get("NAME")).isEqualTo("a webhook");
-    assertThat(migrated.get("URL")).isEqualTo("http://webhook.com");
-    assertThat(migrated.get("PROJECT_UUID")).isNull();
-    assertThat(migrated.get("ORGANIZATION_UUID")).isEqualTo(uuid);
-    assertThat(migrated.get("URL")).isEqualTo("http://webhook.com");
-    assertThat(migrated.get("CREATED_AT")).isEqualTo(system2.now());
-    assertThat(migrated.get("UPDATED_AT")).isEqualTo(system2.now());
+  @DataProvider
+  public static Object[][] numberOfGlobalWebhooksToMigration() {
+    return new Object[][] {
+      {1},
+      {2},
+      {2 + new Random().nextInt(10)}
+    };
   }
 
   @Test
-  public void should_migrate_one_project_webhook() throws SQLException {
-    String organization = insertDefaultOrganization();
-    String projectId = "156";
-    String projectUuid = UuidFactoryFast.getInstance().create();
-    ;
-    insertProject(organization, projectId, projectUuid);
+  public void execute_deletes_inconsistent_properties_for_global_webhook() throws SQLException {
+    String defaultOrganizationUuid = insertDefaultOrganization();
+    insertGlobalWebhookProperties(4);
+    insertGlobalWebhookProperty(1, null, "no name", defaultOrganizationUuid);
+    insertGlobalWebhookProperty(2, "no url", null, defaultOrganizationUuid);
+    insertGlobalWebhookProperty(3, null, null, defaultOrganizationUuid);
+    Webhook webhook = insertGlobalWebhookProperty(4, "name", "url", defaultOrganizationUuid);
+
+    underTest.execute();
+
+    assertThat(selectWebhooksInDb()).containsOnly(new Row(webhook));
+    assertNoMoreWebhookProperties();
+  }
 
-    insertProperty("sonar.webhooks.project", "1", projectId, system2.now());
-    insertProperty("sonar.webhooks.project.1.name", "a webhook", projectId, system2.now());
-    insertProperty("sonar.webhooks.project.1.url", "http://webhook.com", projectId, system2.now());
+  @Test
+  @UseDataProvider("DP_execute_migrates_any_number_of_webhooks_for_any_number_of_existing_project")
+  public void execute_migrates_any_number_of_webhooks_for_any_number_of_existing_project(int webhookCount, int projectCount) throws SQLException {
+    Project[] projects = IntStream.range(0, projectCount)
+      .mapToObj(i -> insertProject(ENABLED))
+      .toArray(Project[]::new);
+    Row[] rows = Arrays.stream(projects).flatMap(project -> {
+      insertProjectWebhookProperties(project, webhookCount);
+      return IntStream.range(1, webhookCount + 1)
+        .mapToObj(i -> insertProjectWebhookProperty(project, i, "name webhook " + i, "url webhook " + i))
+        .map(Row::new);
+    }).toArray(Row[]::new);
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1);
+    assertThat(selectWebhooksInDb()).containsOnly(rows);
+    assertNoMoreWebhookProperties();
+  }
 
-    Map<String, Object> migrated = dbTester.selectFirst("select * from webhooks");
-    assertThat(migrated.get("UUID")).isNotNull();
-    assertThat(migrated.get("NAME")).isEqualTo("a webhook");
-    assertThat(migrated.get("URL")).isEqualTo("http://webhook.com");
-    assertThat(migrated.get("PROJECT_UUID")).isEqualTo(projectUuid);
-    assertThat(migrated.get("ORGANIZATION_UUID")).isNull();
-    assertThat(migrated.get("URL")).isEqualTo("http://webhook.com");
-    assertThat(migrated.get("CREATED_AT")).isEqualTo(system2.now());
-    assertThat(migrated.get("UPDATED_AT")).isEqualTo(system2.now());
+  @DataProvider
+  public static Object[][] DP_execute_migrates_any_number_of_webhooks_for_any_number_of_existing_project() {
+    Random random = new Random();
+    return new Object[][] {
+      {1, 1},
+      {2, 1},
+      {1, 2},
+      {2 + random.nextInt(5), 2 + random.nextInt(5)}
+    };
   }
 
   @Test
-  public void should_migrate_global_webhooks() throws SQLException {
-    insertDefaultOrganization();
-    insertProperty("sonar.webhooks.global", "1,2", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.1.name", "a webhook", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.2.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
+  public void execute_delete_webhooks_of_non_existing_project() throws SQLException {
+    Project project = insertProject(ENABLED);
+    Project nonExistingProject = new Project(233, "foo");
+    Row[] rows = Stream.of(project, nonExistingProject)
+      .map(prj -> {
+        insertProjectWebhookProperties(prj, 1);
+        return insertProjectWebhookProperty(prj, 1, "name", "url");
+      })
+      .map(Row::new)
+      .toArray(Row[]::new);
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2);
+    assertThat(selectWebhooksInDb())
+      .containsOnly(Arrays.stream(rows).filter(r -> Objects.equals(r.projectUuid, project.uuid)).toArray(Row[]::new));
+    assertNoMoreWebhookProperties();
   }
 
   @Test
-  public void should_migrate_only_valid_webhooks() throws SQLException {
-    insertDefaultOrganization();
-    insertProperty("sonar.webhooks.global", "1,2,3,4", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.3.name", "a webhook", null, parseLong(randomNumeric(7)));
-    insertProperty("sonar.webhooks.global.3.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
-    // nothing for 4
+  public void execute_delete_webhooks_of_disabled_project() throws SQLException {
+    Project project = insertProject(ENABLED);
+    Project nonExistingProject = insertProject(DISABLED);
+    Row[] rows = Stream.of(project, nonExistingProject)
+      .map(prj -> {
+        insertProjectWebhookProperties(prj, 1);
+        return insertProjectWebhookProperty(prj, 1, "name", "url");
+      })
+      .map(Row::new)
+      .toArray(Row[]::new);
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1);
+    assertThat(selectWebhooksInDb())
+      .containsOnly(Arrays.stream(rows).filter(r -> Objects.equals(r.projectUuid, project.uuid)).toArray(Row[]::new));
+    assertNoMoreWebhookProperties();
   }
 
   @Test
-  public void should_migrate_project_webhooks() throws SQLException {
-    String organization = insertDefaultOrganization();
-    String projectId = "156";
-    String projectUuid = UuidFactoryFast.getInstance().create();
-    ;
-    insertProject(organization, projectId, projectUuid);
-
-    insertProperty("sonar.webhooks.project", "1,2", projectId, system2.now());
-    insertProperty("sonar.webhooks.project.1.name", "a webhook", projectId, system2.now());
-    insertProperty("sonar.webhooks.project.1.url", "http://webhook.com", projectId, system2.now());
-    insertProperty("sonar.webhooks.project.2.name", "another webhook", projectId, system2.now());
-    insertProperty("sonar.webhooks.project.2.url", "http://webhookhookhook.com", projectId, system2.now());
+  public void execute_deletes_inconsistent_properties_for_project_webhook() throws SQLException {
+    Project project = insertProject(ENABLED);
+    insertProjectWebhookProperties(project, 4);
+    insertProjectWebhookProperty(project, 1, null, "no name");
+    insertProjectWebhookProperty(project, 2, "no url", null);
+    insertProjectWebhookProperty(project, 3, null, null);
+    Webhook webhook = insertProjectWebhookProperty(project, 4, "name", "url");
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2);
+    assertThat(selectWebhooksInDb()).containsOnly(new Row(webhook));
+    assertNoMoreWebhookProperties();
   }
 
   @Test
-  public void should_not_migrate_more_than_10_webhooks_per_project() throws SQLException {
+  @UseDataProvider("DP_execute_delete_webhooks_of_components_which_is_not_a_project")
+  public void execute_delete_webhooks_of_components_which_is_not_a_project(int webhookCount, String scope, String qualifier) throws SQLException {
+    Project project = insertComponent(scope, qualifier, ENABLED);
+    insertProjectWebhookProperties(project, webhookCount);
+    IntStream.range(1, webhookCount + 1)
+      .forEach(i -> insertProjectWebhookProperty(project, i, "name_" + i, "url_" + i));
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0);
+    assertThat(selectWebhooksInDb()).isEmpty();
+    assertNoMoreWebhookProperties();
+  }
+
+  @DataProvider
+  public static Object[][] DP_execute_delete_webhooks_of_components_which_is_not_a_project() {
+    String[] scopes = {Scopes.DIRECTORY, Scopes.FILE};
+    String[] qualifiers = {Qualifiers.VIEW, Qualifiers.SUBVIEW, Qualifiers.MODULE, Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE};
+    int[] webhookCounts = { 1, 2, 2 + new Random().nextInt(5)};
+    Object[][] res = new Object[scopes.length * qualifiers.length * webhookCounts.length][3];
+    int i = 0;
+    for (int webhookCount : webhookCounts) {
+      for (String scope : scopes) {
+        for (String qualifier : qualifiers) {
+          res[i][0] = webhookCount;
+          res[i][1] = scope;
+          res[i][2] = qualifier;
+          i++;
+        }
+      }
+    }
+    return res;
+  }
+
+  @Test
+  public void should_migrate_global_webhooks() throws SQLException {
+    insertDefaultOrganization();
+    insertProperty("sonar.webhooks.global", "1,2", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.1.name", "a webhook", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.2.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
+
+    underTest.execute();
+
+    assertNoMoreWebhookProperties();
+    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(2);
   }
 
   @Test
-  public void should_not_migrate_more_than_10_global_webhooks() throws SQLException {
+  public void should_migrate_only_valid_webhooks() throws SQLException {
+    insertDefaultOrganization();
+    insertProperty("sonar.webhooks.global", "1,2,3,4", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.1.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.2.name", "a webhook", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.3.name", "a webhook", null, parseLong(randomNumeric(7)));
+    insertProperty("sonar.webhooks.global.3.url", "http://webhook.com", null, parseLong(randomNumeric(7)));
+    // nothing for 4
 
     underTest.execute();
 
-    assertThat(dbTester.countRowsOfTable("properties")).isEqualTo(0);
-    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(0);
+    assertNoMoreWebhookProperties();
+    assertThat(dbTester.countRowsOfTable("webhooks")).isEqualTo(1);
   }
 
   private void insertProperty(String key, @Nullable String value, @Nullable String resourceId, Long date) {
@@ -180,7 +264,7 @@ public class MigrateWebhooksToWebhooksTableTest {
       "id", randomNumeric(7),
       "prop_key", valueOf(key),
       "text_value", value,
-      "is_empty", value.isEmpty() ? true : false,
+      "is_empty", value == null || value.isEmpty(),
       "resource_id", resourceId == null ? null : valueOf(resourceId),
       "created_at", valueOf(date));
   }
@@ -195,11 +279,176 @@ public class MigrateWebhooksToWebhooksTableTest {
     return uuid;
   }
 
-  private void insertProject(String organizationUuid, String projectId, String projectUuid) {
+  private static long PROJECT_ID_GENERATOR = new Random().nextInt(343_343);
+
+  private Project insertProject(boolean enabled) {
+    return insertComponent(Scopes.PROJECT, Qualifiers.PROJECT, enabled);
+  }
+
+  private Project insertComponent(String scope, String qualifier, boolean enabled) {
+    long projectId = PROJECT_ID_GENERATOR++;
+    Project res = new Project(projectId, "prj_" + projectId);
     dbTester.executeInsert(
       "PROJECTS",
-      "ID", projectId,
-      "ORGANIZATION_UUID", organizationUuid,
-      "UUID", projectUuid);
+      "ID", res.id,
+      "ORGANIZATION_UUID", randomAlphanumeric(15),
+      "UUID", res.uuid,
+      "ROOT_UUID", res.uuid,
+      "PROJECT_UUID", res.uuid,
+      "UUID_PATH", "." + res.uuid + ".",
+      "PRIVATE", new Random().nextBoolean(),
+      "SCOPE", scope,
+      "QUALIFIER", qualifier,
+      "ENABLED", enabled
+    );
+    return res;
+  }
+
+  private void insertGlobalWebhookProperties(int total) {
+    insertProperty("sonar.webhooks.global",
+      IntStream.range(0, total).map(i -> i + 1).mapToObj(String::valueOf).collect(Collectors.joining(",")),
+      null,
+      NOW);
+  }
+
+  private Webhook insertGlobalWebhookProperty(int i, @Nullable String name, @Nullable String url, String organizationUuid) {
+    long createdAt = NOW + new Random().nextInt(5_6532_999);
+    Webhook res = new Webhook(name, url, organizationUuid, null, createdAt);
+    if (name != null) {
+      insertProperty("sonar.webhooks.global." + i + ".name", name, null, createdAt);
+    }
+    if (url != null) {
+      insertProperty("sonar.webhooks.global." + i + ".url", url, null, createdAt);
+    }
+    return res;
+  }
+
+  private void insertProjectWebhookProperties(Project project, int total) {
+    insertProperty("sonar.webhooks.project",
+      IntStream.range(0, total).map(i -> i + 1).mapToObj(String::valueOf).collect(Collectors.joining(",")),
+      valueOf(project.id),
+      NOW);
+  }
+
+  private Webhook insertProjectWebhookProperty(Project project, int i, @Nullable String name, @Nullable String url) {
+    long createdAt = NOW + new Random().nextInt(5_6532_999);
+    Webhook res = new Webhook(name, url, null, project.uuid, createdAt);
+    if (name != null) {
+      insertProperty("sonar.webhooks.project." + i + ".name", name, valueOf(project.id), createdAt);
+    }
+    if (url != null) {
+      insertProperty("sonar.webhooks.project." + i + ".url", url, valueOf(project.id), createdAt);
+    }
+    return res;
+  }
+
+  private Stream<Row> selectWebhooksInDb() {
+    return dbTester.select("select * from webhooks").stream().map(Row::new);
+  }
+
+  private void assertNoMoreWebhookProperties() {
+    assertThat(dbTester.countSql("select count(*) from properties where prop_key like 'sonar.webhooks.%'"))
+      .isEqualTo(0);
+  }
+
+  private static final class Webhook {
+    @Nullable
+    private final String name;
+    @Nullable
+    private final String url;
+    @Nullable
+    private final String organizationUuid;
+    @Nullable
+    private final String projectUuid;
+    private final long createdAt;
+
+    private Webhook(@Nullable String name, @Nullable String url, @Nullable String organizationUuid, @Nullable String projectUuid, long createdAt) {
+      this.name = name;
+      this.url = url;
+      this.organizationUuid = organizationUuid;
+      this.projectUuid = projectUuid;
+      this.createdAt = createdAt;
+    }
+  }
+
+  private static class Row {
+    private final String uuid;
+    private final String name;
+    private final String url;
+    @Nullable
+    private final String organizationUuid;
+    @Nullable
+    private final String projectUuid;
+    private final long createdAt;
+    private final long updatedAt;
+
+    private Row(Map<String, Object> row) {
+      this.uuid = (String) row.get("UUID");
+      this.name = (String) row.get("NAME");
+      this.url = (String) row.get("URL");
+      this.organizationUuid = (String) row.get("ORGANIZATION_UUID");
+      this.projectUuid = (String) row.get("PROJECT_UUID");
+      this.createdAt = (Long) row.get("CREATED_AT");
+      this.updatedAt = (Long) row.get("UPDATED_AT");
+    }
+
+    private Row(Webhook webhook) {
+      this.uuid = "NOT KNOWN YET";
+      this.name = webhook.name;
+      this.url = webhook.url;
+      this.organizationUuid = webhook.organizationUuid;
+      this.projectUuid = webhook.projectUuid;
+      this.createdAt = webhook.createdAt;
+      this.updatedAt = webhook.createdAt;
+    }
+
+    public String getUuid() {
+      return uuid;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      Row row = (Row) o;
+      return createdAt == row.createdAt &&
+        updatedAt == row.updatedAt &&
+        Objects.equals(name, row.name) &&
+        Objects.equals(url, row.url) &&
+        Objects.equals(organizationUuid, row.organizationUuid) &&
+        Objects.equals(projectUuid, row.projectUuid);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, url, organizationUuid, projectUuid, createdAt, updatedAt);
+    }
+
+    @Override
+    public String toString() {
+      return "Row{" +
+        "uuid='" + uuid + '\'' +
+        ", name='" + name + '\'' +
+        ", url='" + url + '\'' +
+        ", organizationUuid='" + organizationUuid + '\'' +
+        ", projectUuid='" + projectUuid + '\'' +
+        ", createdAt=" + createdAt +
+        ", updatedAt=" + updatedAt +
+        '}';
+    }
+  }
+
+  private static final class Project {
+    private final long id;
+    private final String uuid;
+
+    private Project(long id, String uuid) {
+      this.id = id;
+      this.uuid = uuid;
+    }
   }
 }
index d551e6c37eade0f10596a003e993ce27a79ffa59..a0cd8c422e5d827acd17dac689e090273ee57a0d 100644 (file)
@@ -12,17 +12,17 @@ CREATE TABLE "PROJECTS" (
   "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
   "KEE" VARCHAR(400),
   "UUID" VARCHAR(50) NOT NULL,
---  "UUID_PATH" VARCHAR(1500) NOT NULL,
---  "ROOT_UUID" VARCHAR(50) NOT NULL,
---  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "UUID_PATH" VARCHAR(1500) NOT NULL,
+  "ROOT_UUID" VARCHAR(50) NOT NULL,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
   "MODULE_UUID" VARCHAR(50),
   "MODULE_UUID_PATH" VARCHAR(1500),
   "MAIN_BRANCH_PROJECT_UUID" VARCHAR(50),
   "NAME" VARCHAR(2000),
   "DESCRIPTION" VARCHAR(2000),
---  "PRIVATE" BOOLEAN NOT NULL,
+  "PRIVATE" BOOLEAN NOT NULL,
   "TAGS" VARCHAR(500),
---  "ENABLED" BOOLEAN NOT NULL DEFAULT TRUE,
+  "ENABLED" BOOLEAN NOT NULL DEFAULT TRUE,
   "SCOPE" VARCHAR(3),
   "QUALIFIER" VARCHAR(10),
   "DEPRECATED_KEE" VARCHAR(400),