mirror of
https://github.com/open-metadata/OpenMetadata
synced 2026-05-24 09:39:11 +00:00
Validate only newly-added custom properties; isolate PATCH IT to fresh types
prepare() previously validated the entire customProperties[] on every Type write. An upgraded instance with a legacy property whose name contained a now-banned char would then reject any subsequent PUT/PATCH on that type, even when the write only adds a different valid property. Move the name validation into TypeUpdater.updateCustomProperties() and scope it to the `added` list computed by recordListChange against the original entity. New properties are still validated; pre-existing names are left alone. Replace the IT PATCH cases' shared `topic` Type with a fresh, namespaced entity-category Type per test (createEntityTypeForTest). The shared `topic` was mutated concurrently by other tests in the class — combined with PATCH's lack of per-type locking, that produced lost-update races and flaky asserts. The fresh per-test type has customProperties: [] from creation, so the patch sets the array directly without a seed property.
This commit is contained in:
parent
722f2f24dc
commit
1864b0a6ac
2 changed files with 34 additions and 32 deletions
|
|
@ -485,21 +485,14 @@ public class TypeResourceIT {
|
|||
@Test
|
||||
void test_patchCannotAddCustomPropertyWithDisallowedName(TestNamespace ns) throws Exception {
|
||||
OpenMetadataClient client = SdkClients.adminClient();
|
||||
Type topicType = getTypeByName(client, "topic", "customProperties");
|
||||
|
||||
// Seed a valid property so /customProperties is guaranteed to exist as a non-null array.
|
||||
CustomProperty seed = new CustomProperty();
|
||||
seed.setName(ns.prefix("seedForPatch"));
|
||||
seed.setDescription("Seed for PATCH bypass test");
|
||||
seed.setPropertyType(STRING_TYPE.getEntityReference());
|
||||
addCustomProperty(client, topicType.getId(), seed);
|
||||
Type fresh = createEntityTypeForTest(client, ns, "patchBadType");
|
||||
|
||||
String badName = ns.prefix("patched:bad");
|
||||
String patchJson =
|
||||
String.format(
|
||||
"[{\"op\":\"add\",\"path\":\"/customProperties/-\","
|
||||
+ "\"value\":{\"name\":\"%s\",\"description\":\"probe\","
|
||||
+ "\"propertyType\":{\"id\":\"%s\",\"type\":\"type\",\"name\":\"string\"}}}]",
|
||||
"[{\"op\":\"add\",\"path\":\"/customProperties\","
|
||||
+ "\"value\":[{\"name\":\"%s\",\"description\":\"probe\","
|
||||
+ "\"propertyType\":{\"id\":\"%s\",\"type\":\"type\",\"name\":\"string\"}}]}]",
|
||||
badName, STRING_TYPE.getId());
|
||||
|
||||
assertThrows(
|
||||
|
|
@ -509,14 +502,14 @@ public class TypeResourceIT {
|
|||
.getHttpClient()
|
||||
.executeForString(
|
||||
HttpMethod.PATCH,
|
||||
"/v1/metadata/types/" + topicType.getId(),
|
||||
"/v1/metadata/types/" + fresh.getId(),
|
||||
patchJson,
|
||||
RequestOptions.builder()
|
||||
.header("Content-Type", "application/json-patch+json")
|
||||
.build()),
|
||||
"PATCH that adds a custom property with disallowed character must return 400");
|
||||
|
||||
Type after = getTypeByName(client, "topic", "customProperties");
|
||||
Type after = getTypeById(client, fresh.getId());
|
||||
boolean persisted =
|
||||
after.getCustomProperties() != null
|
||||
&& after.getCustomProperties().stream().anyMatch(cp -> badName.equals(cp.getName()));
|
||||
|
|
@ -526,31 +519,25 @@ public class TypeResourceIT {
|
|||
@Test
|
||||
void test_patchCanAddCustomPropertyWithValidName(TestNamespace ns) throws Exception {
|
||||
OpenMetadataClient client = SdkClients.adminClient();
|
||||
Type topicType = getTypeByName(client, "topic", "customProperties");
|
||||
|
||||
CustomProperty seed = new CustomProperty();
|
||||
seed.setName(ns.prefix("seedForValidPatch"));
|
||||
seed.setDescription("Seed for valid-PATCH test");
|
||||
seed.setPropertyType(STRING_TYPE.getEntityReference());
|
||||
addCustomProperty(client, topicType.getId(), seed);
|
||||
Type fresh = createEntityTypeForTest(client, ns, "patchGoodType");
|
||||
|
||||
String goodName = ns.prefix("patchedGood");
|
||||
String patchJson =
|
||||
String.format(
|
||||
"[{\"op\":\"add\",\"path\":\"/customProperties/-\","
|
||||
+ "\"value\":{\"name\":\"%s\",\"description\":\"probe\","
|
||||
+ "\"propertyType\":{\"id\":\"%s\",\"type\":\"type\",\"name\":\"string\"}}}]",
|
||||
"[{\"op\":\"add\",\"path\":\"/customProperties\","
|
||||
+ "\"value\":[{\"name\":\"%s\",\"description\":\"probe\","
|
||||
+ "\"propertyType\":{\"id\":\"%s\",\"type\":\"type\",\"name\":\"string\"}}]}]",
|
||||
goodName, STRING_TYPE.getId());
|
||||
|
||||
client
|
||||
.getHttpClient()
|
||||
.executeForString(
|
||||
HttpMethod.PATCH,
|
||||
"/v1/metadata/types/" + topicType.getId(),
|
||||
"/v1/metadata/types/" + fresh.getId(),
|
||||
patchJson,
|
||||
RequestOptions.builder().header("Content-Type", "application/json-patch+json").build());
|
||||
|
||||
Type after = getTypeByName(client, "topic", "customProperties");
|
||||
Type after = getTypeById(client, fresh.getId());
|
||||
boolean persisted =
|
||||
after.getCustomProperties() != null
|
||||
&& after.getCustomProperties().stream().anyMatch(cp -> goodName.equals(cp.getName()));
|
||||
|
|
@ -1003,6 +990,21 @@ public class TypeResourceIT {
|
|||
.execute(HttpMethod.POST, "/v1/metadata/types", createRequest, Type.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique entity-category Type per test so PATCH-driven tests can mutate
|
||||
* customProperties without racing against other tests on shared built-in types.
|
||||
*/
|
||||
private static Type createEntityTypeForTest(
|
||||
OpenMetadataClient client, TestNamespace ns, String label) throws Exception {
|
||||
CreateType req = new CreateType();
|
||||
req.setName(ns.prefix(label));
|
||||
req.setCategory(Category.Entity);
|
||||
req.setDescription("Per-test entity type for PATCH IT");
|
||||
req.setNameSpace("data");
|
||||
req.setSchema("{}");
|
||||
return createType(client, req);
|
||||
}
|
||||
|
||||
private static Type getTypeById(OpenMetadataClient client, UUID typeId) throws Exception {
|
||||
String response =
|
||||
client
|
||||
|
|
|
|||
|
|
@ -96,13 +96,6 @@ public class TypeRepository extends EntityRepository<Type> {
|
|||
@Override
|
||||
public void prepare(Type type, boolean update) {
|
||||
TypeRegistry.instance().validateCustomProperties(type);
|
||||
// PATCH carries an opaque JsonPatch; @Valid can't reach into it. Run it ourselves.
|
||||
for (CustomProperty property : listOrEmpty(type.getCustomProperties())) {
|
||||
String violations = ValidatorUtil.validate(property);
|
||||
if (violations != null) {
|
||||
throw new IllegalArgumentException(violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -352,6 +345,13 @@ public class TypeRepository extends EntityRepository<Type> {
|
|||
List<CustomProperty> deleted = new ArrayList<>();
|
||||
recordListChange(
|
||||
"customProperties", origProperties, updatedProperties, added, deleted, customFieldMatch);
|
||||
// Legacy names from existing data are not re-validated; only newly added ones.
|
||||
for (CustomProperty property : added) {
|
||||
String violations = ValidatorUtil.validate(property);
|
||||
if (violations != null) {
|
||||
throw new IllegalArgumentException(violations);
|
||||
}
|
||||
}
|
||||
for (CustomProperty property : added) {
|
||||
storeCustomProperty(property);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue