From: Sébastien Lesaint Date: Mon, 12 Mar 2018 14:49:55 +0000 (+0100) Subject: SONAR-10346 fix DB migration X-Git-Tag: 7.5~1526 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=fe6fcaba75e7ca02678a4ce0dff601b448a2fd7a;p=sonarqube.git SONAR-10346 fix DB migration --- diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java index fb782d5f236..f7894893cca 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTable.java @@ -19,27 +19,30 @@ */ 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 rows = context - .prepareSelect("select id, prop_key, resource_id, text_value, created_at from properties where prop_key like 'sonar.webhooks%'") + Multimap 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> 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 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 rows) throws SQLException { + Multimap rowsByPropertyKey = rows.stream() + .collect(MoreCollectors.index(PropertyRow::getPropertyKey)); + Optional 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 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 extractGlobalWebhooksFrom(Context context, Multimap rowsByPropertyKey, String[] values) throws SQLException { + String defaultOrganizationUuid = defaultOrganizationUuidProvider.get(context); + return Arrays.stream(values) + .map(value -> { + Optional name = rowsByPropertyKey.get("sonar.webhooks.global." + value + ".name").stream().findFirst(); + Optional 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 rows) throws SQLException { + Multimap rowsByPropertyKey = rows.stream() + .collect(MoreCollectors.index(PropertyRow::getPropertyKey)); + Optional 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 extractProjectWebhooksFrom(PropertyRow row, Multimap properties, String[] values) { + return Arrays.stream(values) + .map(value -> { + Optional name = properties.get("sonar.webhooks.project." + value + ".name").stream().findFirst(); + Optional 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 extractGlobalWebhooksFrom(Context context, Map 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 extractProjectWebhooksFrom(Context context, Map properties, String[] values) throws SQLException { - List 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 + '}'; diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java index 6145fa74e0f..1aea7b3aeef 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest.java @@ -19,13 +19,23 @@ */ 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 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 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 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 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; + } } } diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql index d551e6c37ea..a0cd8c422e5 100644 --- a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MigrateWebhooksToWebhooksTableTest/migrate_webhooks.sql @@ -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),