From c15d7b4499cc2bddc3da0b545eba5005ef5d40b2 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:09:27 +0100 Subject: [PATCH 01/11] add rudimentary ResourceDefinitionServiceTest class --- .../ResourceDefinitionServiceTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java new file mode 100644 index 000000000..9d8917572 --- /dev/null +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -0,0 +1,55 @@ +package org.fairdatapoint.service.resource; + +import org.fairdatapoint.BaseIntegrationTest; +import org.fairdatapoint.entity.resource.ResourceDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@AutoConfigureTestEntityManager +@Transactional // required for TestEntityManager outside of @DataJpaTest +public class ResourceDefinitionServiceTest extends BaseIntegrationTest { + + UUID uuid; + + final ResourceDefinitionService resourceDefinitionService; + + final TestEntityManager testEntityManager; + + /** + * Constructor + * @param resourceDefinitionService The service to test + */ + @Autowired + public ResourceDefinitionServiceTest( + ResourceDefinitionService resourceDefinitionService, + TestEntityManager testEntityManager + ) { + this.resourceDefinitionService = resourceDefinitionService; + this.testEntityManager = testEntityManager; + } + + @BeforeEach + public void setUp() { + ResourceDefinition resourceDefinition = new ResourceDefinition(); + resourceDefinition.setName("test"); + resourceDefinition.setUrlPrefix("test"); + resourceDefinition.setChildren(List.of()); + resourceDefinition.setParents(List.of()); + resourceDefinition.setExternalLinks(List.of()); + resourceDefinition.setMetadataSchemaUsages(List.of()); + resourceDefinition = testEntityManager.persist(resourceDefinition); + uuid = resourceDefinition.getUuid(); + } + + @Test + public void testUpdate() { + ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); + } +} From d40b74cb9ec05351d70ec0911934674ce3dd3508 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:19:47 +0100 Subject: [PATCH 02/11] add license to new test class --- .../ResourceDefinitionServiceTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java index 9d8917572..6249e7a4d 100644 --- a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -1,3 +1,25 @@ +/** + * The MIT License + * Copyright © 2016-2024 FAIR Data Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.fairdatapoint.service.resource; import org.fairdatapoint.BaseIntegrationTest; From 679571e46cecb535d4bb7107a0ed7c9c7ab3c23e Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:14:38 +0100 Subject: [PATCH 03/11] initial attempt to reproduce #830 in ResourceDefinitionServiceTest (WIP) --- .../ResourceDefinitionServiceTest.java | 118 ++++++++++++++++-- 1 file changed, 106 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java index 6249e7a4d..0fdae05ce 100644 --- a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -23,22 +23,33 @@ package org.fairdatapoint.service.resource; import org.fairdatapoint.BaseIntegrationTest; +import org.fairdatapoint.api.dto.resource.ResourceDefinitionChangeDTO; +import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildDTO; +import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildListViewDTO; +import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildListViewMetadataDTO; import org.fairdatapoint.entity.resource.ResourceDefinition; +import org.fairdatapoint.entity.resource.ResourceDefinitionChild; +import org.fairdatapoint.entity.resource.ResourceDefinitionChildMetadata; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.BindException; +import java.util.HashMap; import java.util.List; import java.util.UUID; +import static org.junit.jupiter.api.Assertions.assertEquals; + @AutoConfigureTestEntityManager @Transactional // required for TestEntityManager outside of @DataJpaTest public class ResourceDefinitionServiceTest extends BaseIntegrationTest { - UUID uuid; + final HashMap uuids = new HashMap<>(); final ResourceDefinitionService resourceDefinitionService; @@ -46,6 +57,7 @@ public class ResourceDefinitionServiceTest extends BaseIntegrationTest { /** * Constructor + * * @param resourceDefinitionService The service to test */ @Autowired @@ -57,21 +69,103 @@ public ResourceDefinitionServiceTest( this.testEntityManager = testEntityManager; } + /** + * Creates ResourceDefinitionChangeDTO from existing ResourceDefinition + * + * @param uuid ResourceDefinition identifier + * @return Change DTO based on the ResourceDefinition + */ + private ResourceDefinitionChangeDTO createResourceDefinitionChangeDTO(UUID uuid) { + ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); + System.out.println(resourceDefinition.getChildren().size()); // TEMP + final ResourceDefinitionChangeDTO dto = new ResourceDefinitionChangeDTO(); + dto.setName(resourceDefinition.getName()); + dto.setUrlPrefix(resourceDefinition.getUrlPrefix()); + dto.setMetadataSchemaUuids(resourceDefinition.getMetadataSchemaUsages() + .stream() + .map(usage -> usage.getUsedMetadataSchema().getUuid()) + .toList() + ); + dto.setChildren(resourceDefinition.getChildren() + .stream() + .map(child -> new ResourceDefinitionChildDTO( + child.getTarget().getUuid(), + child.getRelationUri(), + new ResourceDefinitionChildListViewDTO( + child.getTitle(), + child.getTagsUri(), + child.getMetadata() + .stream() + .map( + metadata -> new ResourceDefinitionChildListViewMetadataDTO( + metadata.getTitle(), metadata.getPropertyUri() + ) + ).toList() + ) + )) + .toList() + ); + dto.setExternalLinks(List.of()); // TEMP +// TODO: +// dto.setExternalLinks(resourceDefinition.getExternalLinks() +// .stream() +// .map() +// .toList() +// ); + return dto; + } + @BeforeEach public void setUp() { - ResourceDefinition resourceDefinition = new ResourceDefinition(); - resourceDefinition.setName("test"); - resourceDefinition.setUrlPrefix("test"); - resourceDefinition.setChildren(List.of()); - resourceDefinition.setParents(List.of()); - resourceDefinition.setExternalLinks(List.of()); - resourceDefinition.setMetadataSchemaUsages(List.of()); - resourceDefinition = testEntityManager.persist(resourceDefinition); - uuid = resourceDefinition.getUuid(); + // create child resource + ResourceDefinition child = new ResourceDefinition(); + child.setName("child resource"); + child.setUrlPrefix("child"); + child.setChildren(List.of()); + child.setParents(List.of()); + child.setExternalLinks(List.of()); + child.setMetadataSchemaUsages(List.of()); + child = testEntityManager.persist(child); + uuids.put("child", child.getUuid()); + // create parent resource + ResourceDefinition parent = new ResourceDefinition(); + parent.setName("parent resource"); + parent.setUrlPrefix("parent"); + parent.setExternalLinks(List.of()); + parent.setMetadataSchemaUsages(List.of()); + parent = testEntityManager.persist(parent); + uuids.put("parent", parent.getUuid()); + // create parent-child relation + ResourceDefinitionChild relation = new ResourceDefinitionChild(); + relation.setRelationUri("http://example.org/relation"); + relation.setTitle("relation"); + relation.setOrderPriority(1); + relation.setSource(parent); + relation.setTarget(child); + relation = testEntityManager.persist(relation); + uuids.put("relation", relation.getUuid()); + // manage both sides of bidirectional relations + parent.setChildren(List.of(relation)); + child.setParents(List.of(relation)); + // create relation metadata + ResourceDefinitionChildMetadata relationMetadata = new ResourceDefinitionChildMetadata(); + relationMetadata.setTitle("relation metadata"); + relationMetadata.setPropertyUri("http://example.org/property"); + relationMetadata.setOrderPriority(1); + relationMetadata.setChild(relation); + relationMetadata = testEntityManager.persist(relationMetadata); + relation.setMetadata(List.of(relationMetadata)); + // flushed by transaction } @Test - public void testUpdate() { - ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); + @WithMockUser(roles="ADMIN") + public void testUpdate() throws BindException { + final UUID parentUuid = uuids.get("parent"); + ResourceDefinitionChangeDTO changeDTO = createResourceDefinitionChangeDTO(parentUuid); + System.out.println(changeDTO.getName()); + resourceDefinitionService.update(parentUuid, changeDTO); + ResourceDefinition updatedResourceDefinition = testEntityManager.find(ResourceDefinition.class, parentUuid); + assertEquals(1, updatedResourceDefinition.getChildren().size()); } } From 917e60bdf89a80a3075b975327a75a26674a7c21 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:03:24 +0100 Subject: [PATCH 04/11] fix and simplify ResourceDefinitionServiceTest so it now reproduces #830 --- .../ResourceDefinitionServiceTest.java | 116 ++---------------- 1 file changed, 13 insertions(+), 103 deletions(-) diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java index 0fdae05ce..30a8d15f8 100644 --- a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -24,13 +24,8 @@ import org.fairdatapoint.BaseIntegrationTest; import org.fairdatapoint.api.dto.resource.ResourceDefinitionChangeDTO; -import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildDTO; -import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildListViewDTO; -import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildListViewMetadataDTO; import org.fairdatapoint.entity.resource.ResourceDefinition; -import org.fairdatapoint.entity.resource.ResourceDefinitionChild; -import org.fairdatapoint.entity.resource.ResourceDefinitionChildMetadata; -import org.junit.jupiter.api.BeforeEach; +import org.fairdatapoint.util.KnownUUIDs; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager; @@ -40,7 +35,6 @@ import org.springframework.validation.BindException; import java.util.HashMap; -import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -53,6 +47,8 @@ public class ResourceDefinitionServiceTest extends BaseIntegrationTest { final ResourceDefinitionService resourceDefinitionService; + final ResourceDefinitionMapper resourceDefinitionMapper; + final TestEntityManager testEntityManager; /** @@ -63,109 +59,23 @@ public class ResourceDefinitionServiceTest extends BaseIntegrationTest { @Autowired public ResourceDefinitionServiceTest( ResourceDefinitionService resourceDefinitionService, + ResourceDefinitionMapper resourceDefinitionMapper, TestEntityManager testEntityManager ) { this.resourceDefinitionService = resourceDefinitionService; + this.resourceDefinitionMapper = resourceDefinitionMapper; this.testEntityManager = testEntityManager; } - /** - * Creates ResourceDefinitionChangeDTO from existing ResourceDefinition - * - * @param uuid ResourceDefinition identifier - * @return Change DTO based on the ResourceDefinition - */ - private ResourceDefinitionChangeDTO createResourceDefinitionChangeDTO(UUID uuid) { - ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); - System.out.println(resourceDefinition.getChildren().size()); // TEMP - final ResourceDefinitionChangeDTO dto = new ResourceDefinitionChangeDTO(); - dto.setName(resourceDefinition.getName()); - dto.setUrlPrefix(resourceDefinition.getUrlPrefix()); - dto.setMetadataSchemaUuids(resourceDefinition.getMetadataSchemaUsages() - .stream() - .map(usage -> usage.getUsedMetadataSchema().getUuid()) - .toList() - ); - dto.setChildren(resourceDefinition.getChildren() - .stream() - .map(child -> new ResourceDefinitionChildDTO( - child.getTarget().getUuid(), - child.getRelationUri(), - new ResourceDefinitionChildListViewDTO( - child.getTitle(), - child.getTagsUri(), - child.getMetadata() - .stream() - .map( - metadata -> new ResourceDefinitionChildListViewMetadataDTO( - metadata.getTitle(), metadata.getPropertyUri() - ) - ).toList() - ) - )) - .toList() - ); - dto.setExternalLinks(List.of()); // TEMP -// TODO: -// dto.setExternalLinks(resourceDefinition.getExternalLinks() -// .stream() -// .map() -// .toList() -// ); - return dto; - } - - @BeforeEach - public void setUp() { - // create child resource - ResourceDefinition child = new ResourceDefinition(); - child.setName("child resource"); - child.setUrlPrefix("child"); - child.setChildren(List.of()); - child.setParents(List.of()); - child.setExternalLinks(List.of()); - child.setMetadataSchemaUsages(List.of()); - child = testEntityManager.persist(child); - uuids.put("child", child.getUuid()); - // create parent resource - ResourceDefinition parent = new ResourceDefinition(); - parent.setName("parent resource"); - parent.setUrlPrefix("parent"); - parent.setExternalLinks(List.of()); - parent.setMetadataSchemaUsages(List.of()); - parent = testEntityManager.persist(parent); - uuids.put("parent", parent.getUuid()); - // create parent-child relation - ResourceDefinitionChild relation = new ResourceDefinitionChild(); - relation.setRelationUri("http://example.org/relation"); - relation.setTitle("relation"); - relation.setOrderPriority(1); - relation.setSource(parent); - relation.setTarget(child); - relation = testEntityManager.persist(relation); - uuids.put("relation", relation.getUuid()); - // manage both sides of bidirectional relations - parent.setChildren(List.of(relation)); - child.setParents(List.of(relation)); - // create relation metadata - ResourceDefinitionChildMetadata relationMetadata = new ResourceDefinitionChildMetadata(); - relationMetadata.setTitle("relation metadata"); - relationMetadata.setPropertyUri("http://example.org/property"); - relationMetadata.setOrderPriority(1); - relationMetadata.setChild(relation); - relationMetadata = testEntityManager.persist(relationMetadata); - relation.setMetadata(List.of(relationMetadata)); - // flushed by transaction - } - @Test @WithMockUser(roles="ADMIN") - public void testUpdate() throws BindException { - final UUID parentUuid = uuids.get("parent"); - ResourceDefinitionChangeDTO changeDTO = createResourceDefinitionChangeDTO(parentUuid); - System.out.println(changeDTO.getName()); - resourceDefinitionService.update(parentUuid, changeDTO); - ResourceDefinition updatedResourceDefinition = testEntityManager.find(ResourceDefinition.class, parentUuid); - assertEquals(1, updatedResourceDefinition.getChildren().size()); + public void testUpdateDoesNotDuplicateChildren() throws BindException { + // reproduces #830 + final UUID uuid = KnownUUIDs.RD_CATALOG_UUID; + ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); + ResourceDefinitionChangeDTO changeDTO = resourceDefinitionMapper.toChangeDTO(resourceDefinition); + resourceDefinitionService.update(uuid, changeDTO); + testEntityManager.refresh(resourceDefinition); + assertEquals(1, resourceDefinition.getChildren().size()); } } From 33b0135f1e765a6d90e8e6f14f2cdd702ff2e1fb Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:21:41 +0100 Subject: [PATCH 05/11] check all related item counts in ResourceDefinitionServiceTest --- .../ResourceDefinitionServiceTest.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java index 30a8d15f8..d6b791e10 100644 --- a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -67,15 +67,29 @@ public ResourceDefinitionServiceTest( this.testEntityManager = testEntityManager; } + /** + * Reproduces #830 + * @throws BindException + */ @Test @WithMockUser(roles="ADMIN") public void testUpdateDoesNotDuplicateChildren() throws BindException { - // reproduces #830 + // create DTO from existing resource (without making any actual changes) final UUID uuid = KnownUUIDs.RD_CATALOG_UUID; ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); ResourceDefinitionChangeDTO changeDTO = resourceDefinitionMapper.toChangeDTO(resourceDefinition); + // count related items, for reference + final int expectedChildCount = resourceDefinition.getChildren().size(); + final int expectedParentCount = resourceDefinition.getParents().size(); + final int expectedExternalLinkCount = resourceDefinition.getExternalLinks().size(); + final int expectedMetadataSchemaUsageCount = resourceDefinition.getMetadataSchemaUsages().size(); + // call update method resourceDefinitionService.update(uuid, changeDTO); + // check for duplicates (or other inconsistencies in the number of related items) testEntityManager.refresh(resourceDefinition); - assertEquals(1, resourceDefinition.getChildren().size()); + assertEquals(expectedChildCount, resourceDefinition.getChildren().size()); + assertEquals(expectedParentCount, resourceDefinition.getParents().size()); + assertEquals(expectedExternalLinkCount, resourceDefinition.getExternalLinks().size()); + assertEquals(expectedMetadataSchemaUsageCount, resourceDefinition.getMetadataSchemaUsages().size()); } } From a658d4577cab58a1bd0ac2ef90c58b23c66cdd13 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:22:04 +0100 Subject: [PATCH 06/11] remove unused imports from Detail_PUT --- .../java/org/fairdatapoint/acceptance/resource/Detail_PUT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/org/fairdatapoint/acceptance/resource/Detail_PUT.java b/src/test/java/org/fairdatapoint/acceptance/resource/Detail_PUT.java index d4b59c9a1..1366bfc38 100644 --- a/src/test/java/org/fairdatapoint/acceptance/resource/Detail_PUT.java +++ b/src/test/java/org/fairdatapoint/acceptance/resource/Detail_PUT.java @@ -24,7 +24,6 @@ import org.fairdatapoint.WebIntegrationTest; import org.fairdatapoint.api.dto.resource.ResourceDefinitionChangeDTO; -import org.fairdatapoint.api.dto.resource.ResourceDefinitionChildDTO; import org.fairdatapoint.api.dto.resource.ResourceDefinitionDTO; import org.fairdatapoint.database.db.repository.ResourceDefinitionRepository; import org.fairdatapoint.entity.resource.ResourceDefinition; @@ -37,7 +36,6 @@ import org.springframework.http.*; import java.net.URI; -import java.util.List; import java.util.UUID; import static java.lang.String.format; From ed690a2176d6614b0686c2c8f568c09796fb92a1 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:04:40 +0100 Subject: [PATCH 07/11] fix deleteChild so the child is actually deleted The deletion was un-scheduled because the parent and or child entities still had reference to the link entity. This is now fixed by deleting both sides of the relation (see e.g. hibernate docs on bidirectional many-to-many associations). --- .../service/resource/ResourceDefinitionService.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java index c59ec6dc0..2a7e7fc6a 100644 --- a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java +++ b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java @@ -257,7 +257,14 @@ protected void deleteDependents(ResourceDefinition resourceDefinition) { @PreAuthorize("hasRole('ADMIN')") protected void deleteChild(ResourceDefinitionChild child) { childMetadataRepository.deleteAll(child.getMetadata()); - childRepository.delete(child); + // Remember `child` is not actually a child but rather a link entity, + // where `source` is the parent and `target` is the actual child (#821). + // We need to clear both sides of the bi-directional relation. + // todo: refactor after fixing #825 + child.getSource().getChildren().remove(child); + child.getTarget().getParents().remove(child); + child.setSource(null); + child.setTarget(null); } public List getTargetClassUris(ResourceDefinition resourceDefinition) { From 67d032f8ea6d5f698ab306779ef7e55370eb3610 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:06:18 +0100 Subject: [PATCH 08/11] Iterate over a copy of the list of ResourceDefinitionChild items (actually link entities), because we need to remove an item from the original list. --- .../service/resource/ResourceDefinitionService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java index 2a7e7fc6a..8af837b00 100644 --- a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java +++ b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java @@ -246,7 +246,8 @@ protected void createDependents(ResourceDefinition definition, ResourceDefinitio @Transactional @PreAuthorize("hasRole('ADMIN')") protected void deleteDependents(ResourceDefinition resourceDefinition) { - resourceDefinition.getChildren().forEach(this::deleteChild); + // Copy the list of children because we need to modify it (prevent ConcurrentModificationException) + List.copyOf(resourceDefinition.getChildren()).forEach(this::deleteChild); linkRepository.deleteAll(resourceDefinition.getExternalLinks()); usageRepository.deleteAll(resourceDefinition.getMetadataSchemaUsages()); entityManager.flush(); From a21abd1887d2382e9849542e7fc25cc3bcf21402 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:33:24 +0100 Subject: [PATCH 09/11] parameterize ResourceDefinitionServiceTest for duplicated items Iterate over all default resources. The fdp, catalog and dataset reproduce the duplicated children and the distibution reproduces the duplicated external-links from #830 --- .../ResourceDefinitionServiceTest.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java index d6b791e10..1e5d11f38 100644 --- a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -26,7 +26,8 @@ import org.fairdatapoint.api.dto.resource.ResourceDefinitionChangeDTO; import org.fairdatapoint.entity.resource.ResourceDefinition; import org.fairdatapoint.util.KnownUUIDs; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; @@ -36,6 +37,7 @@ import java.util.HashMap; import java.util.UUID; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -67,15 +69,27 @@ public ResourceDefinitionServiceTest( this.testEntityManager = testEntityManager; } + /** + * Provides arguments for @ParameterizedTest + * @return UUID of resource + */ + static Stream uuidProvider() { + return Stream.of( + KnownUUIDs.RD_FDP_UUID, + KnownUUIDs.RD_CATALOG_UUID, + KnownUUIDs.RD_DATASET_UUID, + KnownUUIDs.RD_DISTRIBUTION_UUID + ); + } + /** * Reproduces #830 - * @throws BindException */ - @Test + @ParameterizedTest + @MethodSource("uuidProvider") @WithMockUser(roles="ADMIN") - public void testUpdateDoesNotDuplicateChildren() throws BindException { + public void testUpdateDoesNotDuplicateRelatedItems(UUID uuid) throws BindException { // create DTO from existing resource (without making any actual changes) - final UUID uuid = KnownUUIDs.RD_CATALOG_UUID; ResourceDefinition resourceDefinition = testEntityManager.find(ResourceDefinition.class, uuid); ResourceDefinitionChangeDTO changeDTO = resourceDefinitionMapper.toChangeDTO(resourceDefinition); // count related items, for reference From e26bf0829e83f30090298a498a14c4a58a3c9a3e Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:51:59 +0100 Subject: [PATCH 10/11] clear both sides of ResourceDefinitionLink association to prevent duplicates --- .../service/resource/ResourceDefinitionService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java index 8af837b00..547b4a376 100644 --- a/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java +++ b/src/main/java/org/fairdatapoint/service/resource/ResourceDefinitionService.java @@ -246,9 +246,13 @@ protected void createDependents(ResourceDefinition definition, ResourceDefinitio @Transactional @PreAuthorize("hasRole('ADMIN')") protected void deleteDependents(ResourceDefinition resourceDefinition) { - // Copy the list of children because we need to modify it (prevent ConcurrentModificationException) + // Iterate over list copies because we need to modify the originals (prevent ConcurrentModificationException) + // todo: refactor after fixing #825 List.copyOf(resourceDefinition.getChildren()).forEach(this::deleteChild); - linkRepository.deleteAll(resourceDefinition.getExternalLinks()); + List.copyOf(resourceDefinition.getExternalLinks()).forEach(link -> { + resourceDefinition.getExternalLinks().remove(link); + linkRepository.delete(link); + }); usageRepository.deleteAll(resourceDefinition.getMetadataSchemaUsages()); entityManager.flush(); entityManager.refresh(resourceDefinition); From f78baf95ab40568ef1a978e1524d13b90582130f Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:01:29 +0100 Subject: [PATCH 11/11] verify existence of MetadataSchemaUsage in default resources used in test --- .../service/resource/ResourceDefinitionServiceTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java index 1e5d11f38..047026166 100644 --- a/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java +++ b/src/test/java/org/fairdatapoint/service/resource/ResourceDefinitionServiceTest.java @@ -40,6 +40,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; @AutoConfigureTestEntityManager @Transactional // required for TestEntityManager outside of @DataJpaTest @@ -83,7 +84,8 @@ static Stream uuidProvider() { } /** - * Reproduces #830 + * Reproduces #830, under the assumption that at least one of the default resources has children + * (in this case fdp, catalog, and dataset) and at least one of the resources has external links (distribution). */ @ParameterizedTest @MethodSource("uuidProvider") @@ -97,6 +99,7 @@ public void testUpdateDoesNotDuplicateRelatedItems(UUID uuid) throws BindExcepti final int expectedParentCount = resourceDefinition.getParents().size(); final int expectedExternalLinkCount = resourceDefinition.getExternalLinks().size(); final int expectedMetadataSchemaUsageCount = resourceDefinition.getMetadataSchemaUsages().size(); + assertTrue(expectedMetadataSchemaUsageCount > 0); // call update method resourceDefinitionService.update(uuid, changeDTO); // check for duplicates (or other inconsistencies in the number of related items)