diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/BulkAssetsRemoveDryRunIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/BulkAssetsRemoveDryRunIT.java deleted file mode 100644 index ca6718b42e4..00000000000 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/BulkAssetsRemoveDryRunIT.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright 2021 Collate - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openmetadata.it.tests; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; -import org.openmetadata.it.factories.DatabaseSchemaTestFactory; -import org.openmetadata.it.factories.DatabaseServiceTestFactory; -import org.openmetadata.it.util.SdkClients; -import org.openmetadata.it.util.TestNamespace; -import org.openmetadata.it.util.TestNamespaceExtension; -import org.openmetadata.schema.api.AddGlossaryToAssetsRequest; -import org.openmetadata.schema.api.data.CreateGlossary; -import org.openmetadata.schema.api.data.CreateGlossaryTerm; -import org.openmetadata.schema.api.data.CreateTable; -import org.openmetadata.schema.api.domains.CreateDataProduct; -import org.openmetadata.schema.api.domains.CreateDomain; -import org.openmetadata.schema.api.domains.CreateDomain.DomainType; -import org.openmetadata.schema.api.teams.CreateTeam; -import org.openmetadata.schema.api.teams.CreateTeam.TeamType; -import org.openmetadata.schema.api.teams.CreateUser; -import org.openmetadata.schema.entity.data.Database; -import org.openmetadata.schema.entity.data.DatabaseSchema; -import org.openmetadata.schema.entity.data.Glossary; -import org.openmetadata.schema.entity.data.GlossaryTerm; -import org.openmetadata.schema.entity.data.Table; -import org.openmetadata.schema.entity.domains.DataProduct; -import org.openmetadata.schema.entity.domains.Domain; -import org.openmetadata.schema.entity.services.DatabaseService; -import org.openmetadata.schema.entity.teams.Team; -import org.openmetadata.schema.entity.teams.User; -import org.openmetadata.schema.type.Column; -import org.openmetadata.schema.type.ColumnDataType; -import org.openmetadata.schema.type.TagLabel; -import org.openmetadata.schema.type.TagLabel.TagSource; -import org.openmetadata.schema.type.api.BulkAssets; -import org.openmetadata.schema.type.api.BulkOperationResult; -import org.openmetadata.sdk.client.OpenMetadataClient; -import org.openmetadata.sdk.fluent.Databases; -import org.openmetadata.sdk.network.HttpMethod; - -/** - * Integration tests for dryRun support on synchronous bulk asset remove endpoints: - * - * - * - *

All three previously hardcoded {@code dryRun=false} on the result and proceeded straight to - * the destructive path, so a caller passing {@code dryRun=true} still had the relationship - * removed. These tests pin the corrected behavior alongside the tag-side fix in - * {@link TagBulkAssetsDryRunIT}. - * - *

The tag remove endpoint is async and is covered separately. The endpoints exercised here are - * synchronous, so the tests can assert on the {@link BulkOperationResult} response and on a - * follow-up read in the same test. - */ -@Execution(ExecutionMode.CONCURRENT) -@ExtendWith(TestNamespaceExtension.class) -public class BulkAssetsRemoveDryRunIT { - - // ----------------------------------------------------------------------- - // Glossary term remove (tag_usage path) - // ----------------------------------------------------------------------- - - @Test - void test_glossaryRemove_dryRunTrue_doesNotRemoveTagFromAsset(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - GlossaryTerm term = createGlossaryTerm(ns, client, "dry_run"); - TagLabel termLabel = toGlossaryTagLabel(term); - Table table = createTableWithTag(ns, termLabel); - - AddGlossaryToAssetsRequest dryRunRemove = - new AddGlossaryToAssetsRequest() - .withDryRun(true) - .withAssets(List.of(table.getEntityReference())); - String removePath = "/v1/glossaryTerms/" + term.getId() + "/assets/remove"; - BulkOperationResult result = - client - .getHttpClient() - .execute(HttpMethod.PUT, removePath, dryRunRemove, BulkOperationResult.class); - - assertNotNull(result); - assertTrue(result.getDryRun(), "Result must propagate dryRun=true"); - - Table refreshed = client.tables().get(table.getId().toString(), "tags"); - assertTrue( - hasTag(refreshed, term.getFullyQualifiedName()), - "Glossary tag must still be on the table after a dryRun=true remove"); - } - - @Test - void test_glossaryRemove_dryRunFalse_removesTagFromAsset(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - GlossaryTerm term = createGlossaryTerm(ns, client, "real_remove"); - TagLabel termLabel = toGlossaryTagLabel(term); - Table table = createTableWithTag(ns, termLabel); - - AddGlossaryToAssetsRequest realRemove = - new AddGlossaryToAssetsRequest() - .withDryRun(false) - .withAssets(List.of(table.getEntityReference())); - String removePath = "/v1/glossaryTerms/" + term.getId() + "/assets/remove"; - BulkOperationResult result = - client - .getHttpClient() - .execute(HttpMethod.PUT, removePath, realRemove, BulkOperationResult.class); - - assertNotNull(result); - assertFalse(Boolean.TRUE.equals(result.getDryRun())); - - Table refreshed = client.tables().get(table.getId().toString(), "tags"); - assertFalse( - hasTag(refreshed, term.getFullyQualifiedName()), - "Glossary tag should be removed from the table when dryRun=false"); - } - - // ----------------------------------------------------------------------- - // DataProduct remove (entity_relationship path) - // ----------------------------------------------------------------------- - - @Test - void test_dataProductRemove_dryRunTrue_doesNotDetachAsset(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Domain domain = createDomain(ns, client, "dp_dry_run"); - DataProduct dataProduct = createDataProduct(ns, client, "dp_dry_run", domain); - Table table = createTable(ns); - - BulkAssets addRequest = - new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(false); - String addPath = "/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/add"; - client.getHttpClient().execute(HttpMethod.PUT, addPath, addRequest, BulkOperationResult.class); - - BulkAssets dryRunRemove = - new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(true); - String removePath = - "/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/remove"; - BulkOperationResult result = - client - .getHttpClient() - .execute(HttpMethod.PUT, removePath, dryRunRemove, BulkOperationResult.class); - - assertNotNull(result); - assertTrue(result.getDryRun(), "Result must propagate dryRun=true"); - assertEquals(1, result.getNumberOfRowsProcessed()); - assertEquals(1, result.getNumberOfRowsPassed()); - - Table refreshed = client.tables().get(table.getId().toString(), "dataProducts"); - assertNotNull(refreshed.getDataProducts(), "dataProducts field should be populated"); - assertTrue( - refreshed.getDataProducts().stream().anyMatch(d -> dataProduct.getId().equals(d.getId())), - "Table must still be attached to the data product after a dryRun=true remove"); - } - - @Test - void test_dataProductRemove_dryRunFalse_detachesAsset(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Domain domain = createDomain(ns, client, "dp_real"); - DataProduct dataProduct = createDataProduct(ns, client, "dp_real", domain); - Table table = createTable(ns); - - BulkAssets addRequest = - new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(false); - String addPath = "/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/add"; - client.getHttpClient().execute(HttpMethod.PUT, addPath, addRequest, BulkOperationResult.class); - - BulkAssets realRemove = - new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(false); - String removePath = - "/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/remove"; - BulkOperationResult result = - client - .getHttpClient() - .execute(HttpMethod.PUT, removePath, realRemove, BulkOperationResult.class); - - assertNotNull(result); - assertFalse(Boolean.TRUE.equals(result.getDryRun())); - assertEquals(1, result.getNumberOfRowsPassed()); - - Table refreshed = client.tables().get(table.getId().toString(), "dataProducts"); - assertTrue( - refreshed.getDataProducts() == null - || refreshed.getDataProducts().stream() - .noneMatch(d -> dataProduct.getId().equals(d.getId())), - "Table should no longer be attached to the data product when dryRun=false"); - } - - // ----------------------------------------------------------------------- - // Team remove (entity_relationship path through base bulkAssetsOperation) - // ----------------------------------------------------------------------- - - @Test - void test_teamRemove_dryRunTrue_doesNotDetachUser(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Team team = createTeam(ns, client, "team_dry_run"); - User user = createUser(ns, client, "user_dry_run"); - - BulkAssets addRequest = - new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(false); - String addPath = "/v1/teams/" + team.getName() + "/assets/add"; - client.getHttpClient().execute(HttpMethod.PUT, addPath, addRequest, BulkOperationResult.class); - - BulkAssets dryRunRemove = - new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(true); - String removePath = "/v1/teams/" + team.getName() + "/assets/remove"; - BulkOperationResult result = - client - .getHttpClient() - .execute(HttpMethod.PUT, removePath, dryRunRemove, BulkOperationResult.class); - - assertNotNull(result); - assertTrue(result.getDryRun(), "Result must propagate dryRun=true"); - assertEquals(1, result.getNumberOfRowsProcessed()); - assertEquals(1, result.getNumberOfRowsPassed()); - - User refreshed = client.users().get(user.getId().toString(), "teams"); - assertNotNull(refreshed.getTeams(), "User teams field must be populated"); - assertTrue( - refreshed.getTeams().stream().anyMatch(t -> team.getId().equals(t.getId())), - "User must still belong to the team after a dryRun=true remove"); - } - - @Test - void test_teamRemove_dryRunFalse_detachesUser(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Team team = createTeam(ns, client, "team_real"); - User user = createUser(ns, client, "user_real"); - - BulkAssets addRequest = - new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(false); - String addPath = "/v1/teams/" + team.getName() + "/assets/add"; - client.getHttpClient().execute(HttpMethod.PUT, addPath, addRequest, BulkOperationResult.class); - - BulkAssets realRemove = - new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(false); - String removePath = "/v1/teams/" + team.getName() + "/assets/remove"; - BulkOperationResult result = - client - .getHttpClient() - .execute(HttpMethod.PUT, removePath, realRemove, BulkOperationResult.class); - - assertNotNull(result); - assertFalse(Boolean.TRUE.equals(result.getDryRun())); - assertEquals(1, result.getNumberOfRowsPassed()); - - User refreshed = client.users().get(user.getId().toString(), "teams"); - assertTrue( - refreshed.getTeams() == null - || refreshed.getTeams().stream().noneMatch(t -> team.getId().equals(t.getId())), - "User should no longer belong to the team when dryRun=false"); - } - - // ----------------------------------------------------------------------- - // Helpers - // ----------------------------------------------------------------------- - - private GlossaryTerm createGlossaryTerm( - TestNamespace ns, OpenMetadataClient client, String suffix) { - Glossary glossary = - client - .glossaries() - .create( - new CreateGlossary() - .withName(ns.prefix("glossary_" + suffix)) - .withDescription("Glossary for dryRun remove test")); - return client - .glossaryTerms() - .create( - new CreateGlossaryTerm() - .withName(ns.prefix("term_" + suffix)) - .withGlossary(glossary.getFullyQualifiedName()) - .withDescription("Term for dryRun remove test")); - } - - private TagLabel toGlossaryTagLabel(GlossaryTerm term) { - return new TagLabel() - .withTagFQN(term.getFullyQualifiedName()) - .withSource(TagSource.GLOSSARY) - .withLabelType(TagLabel.LabelType.MANUAL); - } - - private Domain createDomain(TestNamespace ns, OpenMetadataClient client, String suffix) { - return client - .domains() - .create( - new CreateDomain() - .withName(ns.prefix("domain_" + suffix)) - .withDomainType(DomainType.AGGREGATE) - .withDescription("Domain for dryRun remove test")); - } - - private DataProduct createDataProduct( - TestNamespace ns, OpenMetadataClient client, String suffix, Domain domain) { - return client - .dataProducts() - .create( - new CreateDataProduct() - .withName(ns.prefix("dp_" + suffix)) - .withDomains(List.of(domain.getFullyQualifiedName())) - .withDescription("Data product for dryRun remove test")); - } - - private Team createTeam(TestNamespace ns, OpenMetadataClient client, String suffix) { - return client - .teams() - .create( - new CreateTeam() - .withName(ns.prefix("team_" + suffix)) - .withTeamType(TeamType.GROUP) - .withDescription("Team for dryRun remove test")); - } - - private User createUser(TestNamespace ns, OpenMetadataClient client, String suffix) { - String name = ns.prefix("user_" + suffix); - return client - .users() - .create( - new CreateUser() - .withName(name) - .withEmail(name + "@test.om.org") - .withDescription("User for dryRun remove test")); - } - - private Table createTable(TestNamespace ns) throws Exception { - DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns); - Database database = - Databases.create().name(ns.prefix("db")).in(service.getFullyQualifiedName()).execute(); - DatabaseSchema schema = DatabaseSchemaTestFactory.create(ns, database.getFullyQualifiedName()); - - CreateTable createTable = - new CreateTable() - .withName(ns.prefix("table")) - .withDatabaseSchema(schema.getFullyQualifiedName()) - .withColumns( - List.of( - new Column() - .withName("id") - .withDataType(ColumnDataType.BIGINT) - .withDescription("ID column"))); - return SdkClients.adminClient().tables().create(createTable); - } - - private Table createTableWithTag(TestNamespace ns, TagLabel tagLabel) throws Exception { - DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns); - Database database = - Databases.create().name(ns.prefix("db")).in(service.getFullyQualifiedName()).execute(); - DatabaseSchema schema = DatabaseSchemaTestFactory.create(ns, database.getFullyQualifiedName()); - - CreateTable createTable = - new CreateTable() - .withName(ns.prefix("table")) - .withDatabaseSchema(schema.getFullyQualifiedName()) - .withColumns( - List.of( - new Column() - .withName("id") - .withDataType(ColumnDataType.BIGINT) - .withDescription("ID column"))) - .withTags(List.of(tagLabel)); - - Table created = SdkClients.adminClient().tables().create(createTable); - Table fetched = SdkClients.adminClient().tables().get(created.getId().toString(), "tags"); - assertTrue( - hasTag(fetched, tagLabel.getTagFQN()), "Newly created table should already have the tag"); - return fetched; - } - - private boolean hasTag(Table table, String tagFqn) { - return table.getTags() != null - && table.getTags().stream().anyMatch(t -> tagFqn.equals(t.getTagFQN())); - } -} diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java index d12b2750f29..c60c4334939 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java @@ -2993,4 +2993,87 @@ public class DataProductResourceIT extends BaseEntityIT dataProduct.getId().equals(d.getId())), + "Table must still be attached to the data product after dryRun=true remove"); + } + + @Test + void test_bulkRemoveAssets_dryRunFalse_detaches(TestNamespace ns) throws Exception { + Domain domain = createTestDomain(ns, "dr_false_domain"); + DataProduct dataProduct = createDataProductInDomain(ns, domain, "dr_false"); + Table table = createTestTable(ns, "dr_false_tbl", domain); + + addTableToDataProduct(dataProduct, table); + + BulkAssets realRemove = + new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(false); + String removePath = + "/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/remove"; + BulkOperationResult result = + SdkClients.adminClient() + .getHttpClient() + .execute(HttpMethod.PUT, removePath, realRemove, BulkOperationResult.class); + + assertNotNull(result); + assertFalse(Boolean.TRUE.equals(result.getDryRun())); + assertEquals(1, result.getNumberOfRowsPassed()); + + Table refreshed = + SdkClients.adminClient().tables().get(table.getId().toString(), "dataProducts"); + assertTrue( + refreshed.getDataProducts() == null + || refreshed.getDataProducts().stream() + .noneMatch(d -> dataProduct.getId().equals(d.getId())), + "Table should no longer be attached to the data product when dryRun=false"); + } + + private DataProduct createDataProductInDomain(TestNamespace ns, Domain domain, String suffix) { + return SdkClients.adminClient() + .dataProducts() + .create( + new CreateDataProduct() + .withName(ns.prefix("br_dp_" + suffix)) + .withDomains(List.of(domain.getFullyQualifiedName())) + .withDescription("Data product for bulk remove dryRun test")); + } + + private void addTableToDataProduct(DataProduct dataProduct, Table table) throws Exception { + BulkAssets addRequest = + new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(false); + String addPath = "/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/add"; + SdkClients.adminClient() + .getHttpClient() + .execute(HttpMethod.PUT, addPath, addRequest, BulkOperationResult.class); + } } diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java index 39147341f88..050228c1b6c 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -22,6 +23,7 @@ import org.openmetadata.it.factories.DatabaseSchemaTestFactory; import org.openmetadata.it.factories.DatabaseServiceTestFactory; import org.openmetadata.it.util.SdkClients; import org.openmetadata.it.util.TestNamespace; +import org.openmetadata.schema.api.AddGlossaryToAssetsRequest; import org.openmetadata.schema.api.CreateTaskDetails; import org.openmetadata.schema.api.data.CreateGlossary; import org.openmetadata.schema.api.data.CreateGlossaryTerm; @@ -29,6 +31,7 @@ import org.openmetadata.schema.api.data.CreateTable; import org.openmetadata.schema.api.data.TermReference; import org.openmetadata.schema.api.feed.CreateThread; import org.openmetadata.schema.api.teams.CreateUser; +import org.openmetadata.schema.entity.data.Database; import org.openmetadata.schema.entity.data.DatabaseSchema; import org.openmetadata.schema.entity.data.Glossary; import org.openmetadata.schema.entity.data.GlossaryTerm; @@ -36,14 +39,18 @@ import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.entity.feed.Thread; import org.openmetadata.schema.entity.services.DatabaseService; import org.openmetadata.schema.entity.teams.User; +import org.openmetadata.schema.type.Column; +import org.openmetadata.schema.type.ColumnDataType; import org.openmetadata.schema.type.EntityHistory; import org.openmetadata.schema.type.EntityStatus; import org.openmetadata.schema.type.TagLabel; import org.openmetadata.schema.type.TaskType; import org.openmetadata.schema.type.TermRelation; import org.openmetadata.schema.type.ThreadType; +import org.openmetadata.schema.type.api.BulkOperationResult; import org.openmetadata.schema.utils.ResultList; import org.openmetadata.sdk.client.OpenMetadataClient; +import org.openmetadata.sdk.fluent.Databases; import org.openmetadata.sdk.fluent.builders.ColumnBuilder; import org.openmetadata.sdk.models.ListParams; import org.openmetadata.sdk.models.ListResponse; @@ -3244,4 +3251,118 @@ public class GlossaryTermResourceIT extends BaseEntityIT tableHasTag(client, table.getId(), term.getFullyQualifiedName())); + } + + @Test + void test_bulkRemoveGlossaryFromAssets_dryRunFalse_removes(TestNamespace ns) throws Exception { + OpenMetadataClient client = SdkClients.adminClient(); + GlossaryTerm term = createGlossaryTermForBulk(ns, "dr_false"); + Table table = createTableTaggedWithTerm(ns, term, "dr_false"); + + AddGlossaryToAssetsRequest realRemove = + new AddGlossaryToAssetsRequest() + .withDryRun(false) + .withAssets(List.of(table.getEntityReference())); + String path = "/v1/glossaryTerms/" + term.getId() + "/assets/remove"; + BulkOperationResult result = + client.getHttpClient().execute(HttpMethod.PUT, path, realRemove, BulkOperationResult.class); + + assertNotNull(result); + assertFalse(Boolean.TRUE.equals(result.getDryRun())); + assertEquals(1, result.getNumberOfRowsPassed()); + + assertFalse( + tableHasTag(client, table.getId(), term.getFullyQualifiedName()), + "Glossary tag should be removed from table when dryRun=false"); + } + + private GlossaryTerm createGlossaryTermForBulk(TestNamespace ns, String suffix) { + OpenMetadataClient client = SdkClients.adminClient(); + Glossary glossary = + client + .glossaries() + .create( + new CreateGlossary() + .withName(ns.shortPrefix("br_g_" + suffix)) + .withDescription("Glossary for bulk remove dryRun test")); + return client + .glossaryTerms() + .create( + new CreateGlossaryTerm() + .withName(ns.shortPrefix("br_term_" + suffix)) + .withGlossary(glossary.getFullyQualifiedName()) + .withDescription("Term for bulk remove dryRun test")); + } + + private Table createTableTaggedWithTerm(TestNamespace ns, GlossaryTerm term, String suffix) { + OpenMetadataClient client = SdkClients.adminClient(); + DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns); + Database database = + Databases.create() + .name(ns.shortPrefix("br_db_" + suffix)) + .in(service.getFullyQualifiedName()) + .execute(); + DatabaseSchema schema = DatabaseSchemaTestFactory.create(ns, database.getFullyQualifiedName()); + + CreateTable createTable = + new CreateTable() + .withName(ns.shortPrefix("br_tbl_" + suffix)) + .withDatabaseSchema(schema.getFullyQualifiedName()) + .withColumns(List.of(new Column().withName("id").withDataType(ColumnDataType.BIGINT))); + Table table = client.tables().create(createTable); + + TagLabel termLabel = + new TagLabel() + .withTagFQN(term.getFullyQualifiedName()) + .withSource(TagLabel.TagSource.GLOSSARY) + .withLabelType(TagLabel.LabelType.MANUAL) + .withState(TagLabel.State.CONFIRMED); + + Table fetched = client.tables().get(table.getId().toString(), "tags"); + fetched.setTags(List.of(termLabel)); + Table tagged = client.tables().update(table.getId().toString(), fetched); + assertTrue( + tableHasTag(client, table.getId(), term.getFullyQualifiedName()), + "Patched table should already have the glossary term applied"); + return tagged; + } + + private boolean tableHasTag(OpenMetadataClient client, UUID tableId, String tagFqn) { + Table refreshed = client.tables().get(tableId.toString(), "tags"); + return refreshed.getTags() != null + && refreshed.getTags().stream().anyMatch(t -> tagFqn.equals(t.getTagFQN())); + } } diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagBulkAssetsDryRunIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagBulkAssetsDryRunIT.java deleted file mode 100644 index 97614d61195..00000000000 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagBulkAssetsDryRunIT.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2021 Collate - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openmetadata.it.tests; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Duration; -import java.util.List; -import java.util.UUID; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.parallel.Execution; -import org.junit.jupiter.api.parallel.ExecutionMode; -import org.openmetadata.it.factories.DatabaseSchemaTestFactory; -import org.openmetadata.it.factories.DatabaseServiceTestFactory; -import org.openmetadata.it.util.SdkClients; -import org.openmetadata.it.util.TestNamespace; -import org.openmetadata.it.util.TestNamespaceExtension; -import org.openmetadata.schema.api.AddTagToAssetsRequest; -import org.openmetadata.schema.api.classification.CreateClassification; -import org.openmetadata.schema.api.classification.CreateTag; -import org.openmetadata.schema.api.data.CreateTable; -import org.openmetadata.schema.entity.classification.Classification; -import org.openmetadata.schema.entity.classification.Tag; -import org.openmetadata.schema.entity.data.Database; -import org.openmetadata.schema.entity.data.DatabaseSchema; -import org.openmetadata.schema.entity.data.Table; -import org.openmetadata.schema.entity.services.DatabaseService; -import org.openmetadata.schema.type.Column; -import org.openmetadata.schema.type.ColumnDataType; -import org.openmetadata.schema.type.TagLabel; -import org.openmetadata.schema.type.TagLabel.TagSource; -import org.openmetadata.sdk.client.OpenMetadataClient; -import org.openmetadata.sdk.fluent.Databases; -import org.openmetadata.sdk.network.HttpMethod; - -/** - * Integration tests for dryRun support on tag bulk asset add/remove operations - * (PUT /v1/tags/{id}/assets/add and PUT /v1/tags/{id}/assets/remove). - * - *

Covers the fix for issue #27954 where dryRun=true on the remove endpoint - * still removed the tag from the target asset. - * - *

Notes on async behavior: the tag bulk asset endpoints are async — the HTTP - * response is a job id, and the actual mutation runs on a background executor. - * Tests use Awaitility with {@code during(...)} to assert that no mutation occurs - * during a sustained window for dryRun=true, and {@code untilAsserted(...)} to wait - * for the mutation to land for dryRun=false. - */ -@Execution(ExecutionMode.CONCURRENT) -@ExtendWith(TestNamespaceExtension.class) -public class TagBulkAssetsDryRunIT { - - private static final Duration DRY_RUN_NO_OP_WINDOW = Duration.ofSeconds(5); - private static final Duration ASYNC_COMPLETION_TIMEOUT = Duration.ofSeconds(30); - - @Test - void test_dryRunRemove_doesNotRemoveTagFromAsset(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Tag tag = createTag(ns, client, "dry_run_remove"); - TagLabel tagLabel = toTagLabel(tag); - Table table = createTableWithTag(ns, tagLabel); - - AddTagToAssetsRequest dryRunRemove = - new AddTagToAssetsRequest() - .withDryRun(true) - .withAssets(List.of(table.getEntityReference())); - String removePath = "/v1/tags/" + tag.getId() + "/assets/remove"; - client.getHttpClient().execute(HttpMethod.PUT, removePath, dryRunRemove, Void.class); - - UUID tableId = table.getId(); - String tagFqn = tag.getFullyQualifiedName(); - Awaitility.await("Tag must remain on the asset throughout the dryRun window") - .pollDelay(Duration.ofMillis(500)) - .pollInterval(Duration.ofSeconds(1)) - .atMost(DRY_RUN_NO_OP_WINDOW.plusSeconds(5)) - .during(DRY_RUN_NO_OP_WINDOW) - .until(() -> hasTag(client, tableId, tagFqn)); - } - - @Test - void test_dryRunRemove_defaultDryRunTrue_doesNotRemoveTagFromAsset(TestNamespace ns) - throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Tag tag = createTag(ns, client, "default_dry_run"); - TagLabel tagLabel = toTagLabel(tag); - Table table = createTableWithTag(ns, tagLabel); - - AddTagToAssetsRequest defaultDryRun = - new AddTagToAssetsRequest().withAssets(List.of(table.getEntityReference())); - String removePath = "/v1/tags/" + tag.getId() + "/assets/remove"; - client.getHttpClient().execute(HttpMethod.PUT, removePath, defaultDryRun, Void.class); - - UUID tableId = table.getId(); - String tagFqn = tag.getFullyQualifiedName(); - Awaitility.await("Tag must remain on the asset when dryRun field is omitted (defaults to true)") - .pollDelay(Duration.ofMillis(500)) - .pollInterval(Duration.ofSeconds(1)) - .atMost(DRY_RUN_NO_OP_WINDOW.plusSeconds(5)) - .during(DRY_RUN_NO_OP_WINDOW) - .until(() -> hasTag(client, tableId, tagFqn)); - } - - @Test - void test_actualRemove_withoutDryRun_removesTagFromAsset(TestNamespace ns) throws Exception { - OpenMetadataClient client = SdkClients.adminClient(); - - Tag tag = createTag(ns, client, "real_remove"); - TagLabel tagLabel = toTagLabel(tag); - Table table = createTableWithTag(ns, tagLabel); - - AddTagToAssetsRequest realRemove = - new AddTagToAssetsRequest() - .withDryRun(false) - .withAssets(List.of(table.getEntityReference())); - String removePath = "/v1/tags/" + tag.getId() + "/assets/remove"; - client.getHttpClient().execute(HttpMethod.PUT, removePath, realRemove, Void.class); - - UUID tableId = table.getId(); - String tagFqn = tag.getFullyQualifiedName(); - Awaitility.await("Tag should be removed from the asset when dryRun=false") - .pollDelay(Duration.ofMillis(500)) - .pollInterval(Duration.ofSeconds(1)) - .atMost(ASYNC_COMPLETION_TIMEOUT) - .untilAsserted(() -> assertFalse(hasTag(client, tableId, tagFqn))); - } - - // ----------------------------------------------------------------------- - // Helpers - // ----------------------------------------------------------------------- - - private Tag createTag(TestNamespace ns, OpenMetadataClient client, String suffix) { - Classification classification = - client - .classifications() - .create( - new CreateClassification() - .withName(ns.prefix("classification_" + suffix)) - .withDescription("Classification for dryRun remove test")); - - return client - .tags() - .create( - new CreateTag() - .withName(ns.prefix("tag_" + suffix)) - .withClassification(classification.getFullyQualifiedName()) - .withDescription("Tag for dryRun remove test")); - } - - private TagLabel toTagLabel(Tag tag) { - return new TagLabel() - .withTagFQN(tag.getFullyQualifiedName()) - .withSource(TagSource.CLASSIFICATION) - .withLabelType(TagLabel.LabelType.MANUAL); - } - - private Table createTableWithTag(TestNamespace ns, TagLabel tagLabel) throws Exception { - DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns); - Database database = - Databases.create().name(ns.prefix("db")).in(service.getFullyQualifiedName()).execute(); - DatabaseSchema schema = DatabaseSchemaTestFactory.create(ns, database.getFullyQualifiedName()); - - CreateTable createTable = - new CreateTable() - .withName(ns.prefix("table")) - .withDatabaseSchema(schema.getFullyQualifiedName()) - .withColumns( - List.of( - new Column() - .withName("id") - .withDataType(ColumnDataType.BIGINT) - .withDescription("ID column"))) - .withTags(List.of(tagLabel)); - - Table created = SdkClients.adminClient().tables().create(createTable); - Table fetched = SdkClients.adminClient().tables().get(created.getId().toString(), "tags"); - assertNotNull(fetched.getTags(), "Newly created table should expose its tags"); - assertTrue( - fetched.getTags().stream().anyMatch(t -> tagLabel.getTagFQN().equals(t.getTagFQN())), - "Newly created table should already have the test tag applied"); - return fetched; - } - - private boolean hasTag(OpenMetadataClient client, UUID tableId, String tagFqn) { - Table refreshed = client.tables().get(tableId.toString(), "tags"); - return refreshed.getTags() != null - && refreshed.getTags().stream().anyMatch(t -> tagFqn.equals(t.getTagFQN())); - } -} diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagResourceIT.java index bda1062037a..ece5d5884e9 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagResourceIT.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -26,16 +27,19 @@ import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.openmetadata.it.util.SdkClients; import org.openmetadata.it.util.TestNamespace; +import org.openmetadata.schema.api.AddTagToAssetsRequest; import org.openmetadata.schema.api.classification.CreateClassification; import org.openmetadata.schema.api.classification.CreateTag; import org.openmetadata.schema.entity.classification.Classification; import org.openmetadata.schema.entity.classification.Tag; import org.openmetadata.schema.entity.data.DatabaseSchema; +import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.type.AssetCertification; import org.openmetadata.schema.type.EntityHistory; import org.openmetadata.schema.type.Paging; import org.openmetadata.schema.type.PredefinedRecognizer; import org.openmetadata.schema.type.Recognizer; +import org.openmetadata.schema.type.TagLabel; import org.openmetadata.schema.utils.ResultList; import org.openmetadata.sdk.client.OpenMetadataClient; import org.openmetadata.sdk.exceptions.InvalidRequestException; @@ -1893,4 +1897,104 @@ public class TagResourceIT extends BaseEntityIT { "Owner should match the user set on the classification"); }); } + + // =================================================================== + // BULK REMOVE TAG FROM ASSETS — dryRun behavior (issue #27954) + // =================================================================== + + @Test + void test_bulkRemoveTagFromAssets_dryRunTrue_doesNotRemove(TestNamespace ns) throws Exception { + OpenMetadataClient client = SdkClients.adminClient(); + Tag tag = createTagForBulk(ns, "dr_true"); + Table table = createTableTaggedWith(ns, tag, "dr_true"); + + AddTagToAssetsRequest dryRunRemove = + new AddTagToAssetsRequest() + .withDryRun(true) + .withAssets(List.of(table.getEntityReference())); + String path = "/v1/tags/" + tag.getId() + "/assets/remove"; + client.getHttpClient().execute(HttpMethod.PUT, path, dryRunRemove, Void.class); + + UUID tableId = table.getId(); + String tagFqn = tag.getFullyQualifiedName(); + Awaitility.await("Tag must remain on asset throughout dryRun window") + .pollDelay(Duration.ofSeconds(1)) + .pollInterval(Duration.ofSeconds(2)) + .atMost(Duration.ofSeconds(45)) + .during(Duration.ofSeconds(20)) + .until(() -> tableHasTag(client, tableId, tagFqn)); + } + + @Test + void test_bulkRemoveTagFromAssets_dryRunFalse_removes(TestNamespace ns) throws Exception { + OpenMetadataClient client = SdkClients.adminClient(); + Tag tag = createTagForBulk(ns, "dr_false"); + Table table = createTableTaggedWith(ns, tag, "dr_false"); + + AddTagToAssetsRequest realRemove = + new AddTagToAssetsRequest() + .withDryRun(false) + .withAssets(List.of(table.getEntityReference())); + String path = "/v1/tags/" + tag.getId() + "/assets/remove"; + client.getHttpClient().execute(HttpMethod.PUT, path, realRemove, Void.class); + + UUID tableId = table.getId(); + String tagFqn = tag.getFullyQualifiedName(); + Awaitility.await("Tag should be removed from asset when dryRun=false") + .pollDelay(Duration.ofMillis(500)) + .pollInterval(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(45)) + .untilAsserted(() -> assertFalse(tableHasTag(client, tableId, tagFqn))); + } + + private Tag createTagForBulk(TestNamespace ns, String suffix) { + Classification classification = createClassification(ns); + CreateTag createTag = new CreateTag(); + createTag.setName(ns.shortPrefix("br_" + suffix)); + createTag.setClassification(classification.getFullyQualifiedName()); + createTag.setDescription("Tag for bulk remove dryRun test"); + return createEntity(createTag); + } + + private Table createTableTaggedWith(TestNamespace ns, Tag tag, String suffix) { + OpenMetadataClient client = SdkClients.adminClient(); + org.openmetadata.schema.entity.services.DatabaseService dbService = + createDatabaseService(ns, "br_svc_" + suffix); + org.openmetadata.schema.entity.data.Database db = + createDatabase(ns, dbService.getFullyQualifiedName()); + DatabaseSchema schema = createDatabaseSchema(ns, db.getFullyQualifiedName()); + + org.openmetadata.schema.api.data.CreateTable createTable = + new org.openmetadata.schema.api.data.CreateTable(); + createTable.setName(ns.shortPrefix("br_tbl_" + suffix)); + createTable.setDatabaseSchema(schema.getFullyQualifiedName()); + createTable.setColumns( + List.of( + new org.openmetadata.schema.type.Column() + .withName("id") + .withDataType(org.openmetadata.schema.type.ColumnDataType.BIGINT))); + Table table = client.tables().create(createTable); + + TagLabel tagLabel = + new TagLabel() + .withTagFQN(tag.getFullyQualifiedName()) + .withSource(TagLabel.TagSource.CLASSIFICATION) + .withLabelType(TagLabel.LabelType.MANUAL) + .withState(TagLabel.State.CONFIRMED); + + Table fetched = client.tables().get(table.getId().toString(), "tags"); + fetched.setTags(List.of(tagLabel)); + Table tagged = client.tables().update(table.getId().toString(), fetched); + assertNotNull(tagged.getTags(), "Patched table should expose tags"); + assertTrue( + tableHasTag(client, table.getId(), tag.getFullyQualifiedName()), + "Patched table should already have the tag applied"); + return tagged; + } + + private boolean tableHasTag(OpenMetadataClient client, UUID tableId, String tagFqn) { + Table refreshed = client.tables().get(tableId.toString(), "tags"); + return refreshed.getTags() != null + && refreshed.getTags().stream().anyMatch(t -> tagFqn.equals(t.getTagFQN())); + } } diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TeamResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TeamResourceIT.java index d08be1b690a..20cc797f899 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TeamResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TeamResourceIT.java @@ -1458,4 +1458,70 @@ public class TeamResourceIT extends BaseEntityIT { "Team policies count should match"); } } + + // =================================================================== + // BULK REMOVE ASSETS — dryRun behavior (issue #27954) + // =================================================================== + + @Test + void test_bulkRemoveAssets_dryRunTrue_doesNotDetachUser(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + Team team = createTeam(ns, "dr_true"); + User user = createTestUser(ns, "dr_true_user"); + + BulkAssets addRequest = + new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(false); + bulkAddAssetsWithResult(client, team.getName(), addRequest); + + BulkAssets dryRunRemove = + new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(true); + BulkOperationResult result = bulkRemoveAssetsWithResult(client, team.getName(), dryRunRemove); + + assertNotNull(result); + assertTrue(result.getDryRun(), "Result must propagate dryRun=true"); + assertEquals(1, result.getNumberOfRowsProcessed()); + assertEquals(1, result.getNumberOfRowsPassed()); + + User refreshed = client.users().get(user.getId().toString(), "teams"); + assertNotNull(refreshed.getTeams(), "User teams field must be populated"); + assertTrue( + refreshed.getTeams().stream().anyMatch(t -> team.getId().equals(t.getId())), + "User must still belong to the team after dryRun=true remove"); + } + + @Test + void test_bulkRemoveAssets_dryRunFalse_detachesUser(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + Team team = createTeam(ns, "dr_false"); + User user = createTestUser(ns, "dr_false_user"); + + BulkAssets addRequest = + new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(false); + bulkAddAssetsWithResult(client, team.getName(), addRequest); + + BulkAssets realRemove = + new BulkAssets().withAssets(List.of(user.getEntityReference())).withDryRun(false); + BulkOperationResult result = bulkRemoveAssetsWithResult(client, team.getName(), realRemove); + + assertNotNull(result); + assertFalse(Boolean.TRUE.equals(result.getDryRun())); + assertEquals(1, result.getNumberOfRowsPassed()); + + User refreshed = client.users().get(user.getId().toString(), "teams"); + assertTrue( + refreshed.getTeams() == null + || refreshed.getTeams().stream().noneMatch(t -> team.getId().equals(t.getId())), + "User should no longer belong to the team when dryRun=false"); + } + + private Team createTeam(TestNamespace ns, String suffix) { + return SdkClients.adminClient() + .teams() + .create( + new CreateTeam() + .withName(ns.prefix("br_team_" + suffix)) + .withTeamType(TeamType.GROUP) + .withProfile(PROFILE) + .withDescription("Team for bulk remove dryRun test")); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java index 0fd91cac0ab..41ba2d465a7 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java @@ -1372,16 +1372,24 @@ public class GlossaryTermRepository extends EntityRepository { new BulkOperationResult().withStatus(ApiStatus.SUCCESS).withDryRun(dryRun); List success = new ArrayList<>(); - if (dryRun || nullOrEmpty(request.getAssets())) { + if (nullOrEmpty(request.getAssets())) { // Nothing to Validate - return result - .withStatus(ApiStatus.SUCCESS) - .withSuccessRequest(List.of(new BulkResponse().withMessage("Nothing to Validate."))); + return result.withSuccessRequest( + List.of(new BulkResponse().withMessage("Nothing to Validate."))); } // Validation for entityReferences EntityUtil.populateEntityReferences(request.getAssets()); + if (dryRun) { + for (EntityReference ref : request.getAssets()) { + result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1); + success.add(new BulkResponse().withRequest(ref)); + result.setNumberOfRowsPassed(result.getNumberOfRowsPassed() + 1); + } + return result.withSuccessRequest(success); + } + for (EntityReference ref : request.getAssets()) { // Update Result Processed result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java index 94292fe2c62..fb60fc3d8d2 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java @@ -503,8 +503,8 @@ public class TagRepository extends EntityRepository { @Override public BulkOperationResult bulkRemoveAndValidateTagsToAssets( UUID classificationTagId, BulkAssetsRequestInterface request) { - AddTagToAssetsRequest addTagToAssetsRequest = (AddTagToAssetsRequest) request; - boolean dryRun = Boolean.TRUE.equals(addTagToAssetsRequest.getDryRun()); + AddTagToAssetsRequest assetsRequest = (AddTagToAssetsRequest) request; + boolean dryRun = Boolean.TRUE.equals(assetsRequest.getDryRun()); Tag tag = this.get(null, classificationTagId, getFields("id")); @@ -512,16 +512,24 @@ public class TagRepository extends EntityRepository { new BulkOperationResult().withStatus(ApiStatus.SUCCESS).withDryRun(dryRun); List success = new ArrayList<>(); - if (dryRun || nullOrEmpty(request.getAssets())) { + if (nullOrEmpty(request.getAssets())) { // Nothing to Validate - return result - .withStatus(ApiStatus.SUCCESS) - .withSuccessRequest(List.of(new BulkResponse().withMessage("Nothing to Validate."))); + return result.withSuccessRequest( + List.of(new BulkResponse().withMessage("Nothing to Validate."))); } // Validation for entityReferences EntityUtil.populateEntityReferences(request.getAssets()); + if (dryRun) { + for (EntityReference ref : request.getAssets()) { + result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1); + success.add(new BulkResponse().withRequest(ref)); + result.setNumberOfRowsPassed(result.getNumberOfRowsPassed() + 1); + } + return result.withSuccessRequest(success); + } + for (EntityReference ref : request.getAssets()) { // Update Result Processed result.setNumberOfRowsProcessed(result.getNumberOfRowsProcessed() + 1);