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:
sonika-shah 2026-05-05 18:01:10 +05:30
parent 722f2f24dc
commit 1864b0a6ac
2 changed files with 34 additions and 32 deletions

View file

@ -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

View file

@ -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);
}