mirror of
https://github.com/open-metadata/OpenMetadata
synced 2026-05-24 09:39:11 +00:00
test(bulk-assets): move dryRun remove tests into existing IT files
Address review feedback:
- Drop the standalone TagBulkAssetsDryRunIT / BulkAssetsRemoveDryRunIT
and fold the dryRun=true / dryRun=false coverage into the per-entity
IT files that already own each endpoint:
* TagResourceIT — tag bulk remove (async) with Awaitility during(...)
window
* GlossaryTermResourceIT — glossary term bulk remove (sync)
* DataProductResourceIT — data product bulk remove (sync), domains
properly aligned so add validation passes
* TeamResourceIT — team bulk remove (sync), reusing the existing
sanitized createTestUser helper to avoid the email-too-long /
invalid-chars failures the standalone file hit
- Rename the misleading addTagToAssetsRequest local in
TagRepository.bulkRemoveAndValidateTagsToAssets to assetsRequest
(copilot review)
- Make the dryRun branch in TagRepository / GlossaryTermRepository
iterate per asset and return one BulkResponse per request entry,
matching the DataProduct/Team paths so callers get a real preview
with rowsProcessed / rowsPassed counts (gitar-bot review)
This commit is contained in:
parent
ce62feab04
commit
59e6572bca
8 changed files with 400 additions and 614 deletions
|
|
@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>PUT /v1/glossaryTerms/{id}/assets/remove
|
||||
* <li>PUT /v1/dataProducts/{name}/assets/remove
|
||||
* <li>PUT /v1/teams/{name}/assets/remove
|
||||
* </ul>
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -2993,4 +2993,87 @@ public class DataProductResourceIT extends BaseEntityIT<DataProduct, CreateDataP
|
|||
listed.getOwners() == null || listed.getOwners().isEmpty(),
|
||||
"Soft-deleted owner must not appear in list endpoint");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// BULK REMOVE ASSETS — dryRun behavior (issue #27954)
|
||||
// ===================================================================
|
||||
|
||||
@Test
|
||||
void test_bulkRemoveAssets_dryRunTrue_doesNotDetach(TestNamespace ns) throws Exception {
|
||||
Domain domain = createTestDomain(ns, "dr_true_domain");
|
||||
DataProduct dataProduct = createDataProductInDomain(ns, domain, "dr_true");
|
||||
Table table = createTestTable(ns, "dr_true_tbl", domain);
|
||||
|
||||
addTableToDataProduct(dataProduct, table);
|
||||
|
||||
BulkAssets dryRunRemove =
|
||||
new BulkAssets().withAssets(List.of(table.getEntityReference())).withDryRun(true);
|
||||
String removePath =
|
||||
"/v1/dataProducts/" + dataProduct.getFullyQualifiedName() + "/assets/remove";
|
||||
BulkOperationResult result =
|
||||
SdkClients.adminClient()
|
||||
.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 =
|
||||
SdkClients.adminClient().tables().get(table.getId().toString(), "dataProducts");
|
||||
assertNotNull(refreshed.getDataProducts(), "dataProducts field must be populated");
|
||||
assertTrue(
|
||||
refreshed.getDataProducts().stream().anyMatch(d -> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GlossaryTerm, CreateGlo
|
|||
listed.getReviewers() == null || listed.getReviewers().isEmpty(),
|
||||
"Soft-deleted reviewer must not appear in list endpoint");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// BULK REMOVE GLOSSARY FROM ASSETS — dryRun behavior (issue #27954)
|
||||
// ===================================================================
|
||||
|
||||
@Test
|
||||
void test_bulkRemoveGlossaryFromAssets_dryRunTrue_doesNotRemove(TestNamespace ns)
|
||||
throws Exception {
|
||||
OpenMetadataClient client = SdkClients.adminClient();
|
||||
GlossaryTerm term = createGlossaryTermForBulk(ns, "dr_true");
|
||||
Table table = createTableTaggedWithTerm(ns, term, "dr_true");
|
||||
|
||||
AddGlossaryToAssetsRequest dryRunRemove =
|
||||
new AddGlossaryToAssetsRequest()
|
||||
.withDryRun(true)
|
||||
.withAssets(List.of(table.getEntityReference()));
|
||||
String path = "/v1/glossaryTerms/" + term.getId() + "/assets/remove";
|
||||
BulkOperationResult result =
|
||||
client
|
||||
.getHttpClient()
|
||||
.execute(HttpMethod.PUT, path, dryRunRemove, BulkOperationResult.class);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getDryRun(), "Result must propagate dryRun=true");
|
||||
assertEquals(1, result.getNumberOfRowsProcessed());
|
||||
assertEquals(1, result.getNumberOfRowsPassed());
|
||||
|
||||
Awaitility.await("Glossary tag must remain on table after dryRun=true remove")
|
||||
.pollDelay(Duration.ofMillis(500))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.atMost(Duration.ofSeconds(15))
|
||||
.during(Duration.ofSeconds(5))
|
||||
.until(() -> 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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
*
|
||||
* <p>Covers the fix for issue #27954 where dryRun=true on the remove endpoint
|
||||
* still removed the tag from the target asset.
|
||||
*
|
||||
* <p>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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Tag, CreateTag> {
|
|||
"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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1458,4 +1458,70 @@ public class TeamResourceIT extends BaseEntityIT<Team, CreateTeam> {
|
|||
"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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1372,16 +1372,24 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
|||
new BulkOperationResult().withStatus(ApiStatus.SUCCESS).withDryRun(dryRun);
|
||||
List<BulkResponse> 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);
|
||||
|
|
|
|||
|
|
@ -503,8 +503,8 @@ public class TagRepository extends EntityRepository<Tag> {
|
|||
@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<Tag> {
|
|||
new BulkOperationResult().withStatus(ApiStatus.SUCCESS).withDryRun(dryRun);
|
||||
List<BulkResponse> 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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue