Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0a24e14
draft merge some modifications into a new composite
Mathieu-Deharbe Apr 30, 2026
166a8bc
mergeNetworkModificationsIntoNewComposite
Mathieu-Deharbe May 5, 2026
cf68049
Merge branch 'main' into merge-modifications-into-composite
Mathieu-Deharbe May 13, 2026
5e7311f
corrections post merge
Mathieu-Deharbe May 13, 2026
72a768a
remove auto application + send UUID + adept to refacto
Mathieu-Deharbe May 15, 2026
49a522a
frees merges modifications from its groups
Mathieu-Deharbe May 15, 2026
028a31c
cleans the composites whose submodifications are merged into a new one
Mathieu-Deharbe May 15, 2026
db56d65
simplify endpoint
Mathieu-Deharbe May 15, 2026
63f60b8
testMergeNetworkModificationsIntoNewComposite
Mathieu-Deharbe May 18, 2026
261f807
remove useless line
Mathieu-Deharbe May 18, 2026
b9b7e05
test handles more cases
Mathieu-Deharbe May 18, 2026
97b0370
simplify endpoint
Mathieu-Deharbe May 19, 2026
790ce10
Merge branch 'main' into merge-modifications-into-composite
Mathieu-Deharbe May 26, 2026
59a9005
rename
Mathieu-Deharbe May 26, 2026
53ea896
Merge remote-tracking branch 'origin/merge-modifications-into-composi…
Mathieu-Deharbe May 26, 2026
4f216da
correct TU endpoint
Mathieu-Deharbe May 26, 2026
abf3768
Merge branch 'main' into merge-modifications-into-composite
Mathieu-Deharbe May 28, 2026
3771f0b
rename merge to assemble
Mathieu-Deharbe May 28, 2026
daabea4
better order management
Mathieu-Deharbe May 28, 2026
9e03562
Merge branch 'main' into merge-modifications-into-composite
Mathieu-Deharbe May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ public ResponseEntity<Void> moveSubModification(
return ResponseEntity.ok().build();
}

@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Assemble some network modifications into a new composite modification")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been created")})
public ResponseEntity<UUID> assembleNetworkModificationsIntoNewComposite(
@RequestBody List<UUID> assembledModificationsUuids) {
return ResponseEntity.ok().body(
networkModificationService.assembleNetworkModificationsIntoNewComposite(assembledModificationsUuids)
);
}

@PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Create a network composite modification")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been created")})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@ public void setModifications(List<ModificationEntity> modifications) {
modifications.forEach(modification ->
modification.setGroup(this)
);
for (int i = 0; i < this.getModifications().size(); i++) {
this.getModifications().get(i).setModificationsOrder(i);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ SELECT CAST(sm.modification_id AS VARCHAR)
""", nativeQuery = true)
List<UUID> findModificationIdsByCompositeModificationId(UUID uuid);

// return the uuid of the composite containing the modification sent as parameter
@Query(value = """
SELECT CAST(sm.id AS VARCHAR)
FROM composite_modification_sub_modifications sm
INNER JOIN modification m ON sm.modification_id = m.id
WHERE sm.modification_id = :uuid
ORDER BY m.modifications_order
""", nativeQuery = true)
UUID findCompositeIdByContainedModificationId(UUID uuid);
Comment on lines +74 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle ambiguous ownership explicitly in findCompositeIdByContainedModificationId.

This query can return multiple sm.id values for one modification_id, but the method returns a single UUID. That creates a runtime failure path and hides data-integrity issues.

Suggested change
-    UUID findCompositeIdByContainedModificationId(UUID uuid);
+    List<UUID> findCompositeIdsByContainedModificationId(UUID uuid);

Then validate in caller:

  • 0 result: no owner
  • 1 result: expected
  • >1 result: throw a domain error (MODIFICATION_ERROR) with clear message
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/gridsuite/modification/server/repositories/ModificationRepository.java`
around lines 74 - 82, The method findCompositeIdByContainedModificationId
currently returns a single UUID but the query on
composite_modification_sub_modifications can yield multiple sm.id rows; change
the repository method signature to return List<UUID> (or List<String> if
casting) so all matching composite IDs are returned, and then update the
caller(s) to validate the result: if size==0 treat as "no owner", if size==1 use
that UUID, and if size>1 throw a domain error (MODIFICATION_ERROR) with a clear
message indicating ambiguous ownership; keep references to the repository method
name (findCompositeIdByContainedModificationId), the
composite_modification_sub_modifications table and sm.id in your validation
logic so it’s easy to trace.


@Query(value = """
SELECT CAST(sm.modification_id AS VARCHAR)
FROM composite_modification_sub_modifications sm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,76 @@ public List<ModificationInfos> insertCompositeModifications(
return newEntities.stream().map(ModificationEntity::toModificationInfos).toList();
}

@Transactional
public CompositeModificationEntity assembleNetworkModificationsIntoNewComposite(List<UUID> assembledModificationsUuids) {
// get the target (groupUuid or composite Uuid of the first assembled modification + its index in this target)
final UUID firstModifUuid = assembledModificationsUuids.getFirst();
final ModificationEntity firstModificationEntity = getModificationEntity(firstModifUuid);
final int targetIndex = firstModificationEntity.getModificationsOrder();
ModificationGroupEntity targetGroup = firstModificationEntity.getGroup();
CompositeModificationEntity targetComposite = null;
if (targetGroup == null) {
// the first modification is inside a composite
UUID targetCompositeUuid = modificationRepository.findCompositeIdByContainedModificationId(firstModifUuid);
targetComposite = compositeModificationRepository.findById(targetCompositeUuid).orElse(null);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion :
It can be refactored maybe , extract small methods to simplify


// get all the modifications to be assembled, remove previous assignment
List<ModificationEntity> assembledModifications = assembledModificationsUuids.stream()
.map(modificationRepository::findById).filter(Optional::isPresent).map(Optional::get).toList();
// 1. cleans and reorders the origin group if there is one :
Comment on lines +915 to +929
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate input list and fail on missing UUIDs instead of partial assembly.

Current flow throws on empty list (getFirst) and silently drops missing IDs (filter(Optional::isPresent)), which can produce partial, surprising results.

Suggested validation guard
+        if (assembledModificationsUuids == null || assembledModificationsUuids.isEmpty()) {
+            throw new NetworkModificationException(MODIFICATION_ERROR, "No modifications provided for assembly");
+        }
...
-        List<ModificationEntity> assembledModifications = assembledModificationsUuids.stream()
-                .map(modificationRepository::findById).filter(Optional::isPresent).map(Optional::get).toList();
+        List<ModificationEntity> assembledModifications = assembledModificationsUuids.stream()
+                .map(uuid -> modificationRepository.findById(uuid)
+                        .orElseThrow(() -> new NetworkModificationException(MODIFICATION_NOT_FOUND,
+                                String.format(MODIFICATION_NOT_FOUND_MESSAGE, uuid))))
+                .toList();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final UUID firstModifUuid = assembledModificationsUuids.getFirst();
final ModificationEntity firstModificationEntity = getModificationEntity(firstModifUuid);
final int targetIndex = firstModificationEntity.getModificationsOrder();
ModificationGroupEntity targetGroup = firstModificationEntity.getGroup();
CompositeModificationEntity targetComposite = null;
if (targetGroup == null) {
// the first modification is inside a composite
UUID targetCompositeUuid = modificationRepository.findCompositeIdByContainedModificationId(firstModifUuid);
targetComposite = compositeModificationRepository.findById(targetCompositeUuid).orElse(null);
}
// get all the modifications to be assembled, remove previous assignment
List<ModificationEntity> assembledModifications = assembledModificationsUuids.stream()
.map(modificationRepository::findById).filter(Optional::isPresent).map(Optional::get).toList();
// 1. cleans and reorders the origin group if there is one :
if (assembledModificationsUuids == null || assembledModificationsUuids.isEmpty()) {
throw new NetworkModificationException(MODIFICATION_ERROR, "No modifications provided for assembly");
}
final UUID firstModifUuid = assembledModificationsUuids.getFirst();
final ModificationEntity firstModificationEntity = getModificationEntity(firstModifUuid);
final int targetIndex = firstModificationEntity.getModificationsOrder();
ModificationGroupEntity targetGroup = firstModificationEntity.getGroup();
CompositeModificationEntity targetComposite = null;
if (targetGroup == null) {
// the first modification is inside a composite
UUID targetCompositeUuid = modificationRepository.findCompositeIdByContainedModificationId(firstModifUuid);
targetComposite = compositeModificationRepository.findById(targetCompositeUuid).orElse(null);
}
// get all the modifications to be assembled, remove previous assignment
List<ModificationEntity> assembledModifications = assembledModificationsUuids.stream()
.map(uuid -> modificationRepository.findById(uuid)
.orElseThrow(() -> new NetworkModificationException(MODIFICATION_NOT_FOUND,
String.format(MODIFICATION_NOT_FOUND_MESSAGE, uuid))))
.toList();
// 1. cleans and reorders the origin group if there is one :
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/gridsuite/modification/server/repositories/NetworkModificationRepository.java`
around lines 915 - 929, The code currently assumes assembledModificationsUuids
has elements and silently drops missing IDs when mapping via
modificationRepository.findById; add input validation at the start of the method
to (1) check assembledModificationsUuids is non-empty and throw a clear
IllegalArgumentException (or domain-specific exception) if empty instead of
calling getFirst(), and (2) after mapping UUIDs with
modificationRepository.findById, verify that the count of found
ModificationEntity objects equals the input UUID list size and if not throw an
exception listing the missing UUIDs (compare assembledModificationsUuids to the
found IDs) rather than using filter(Optional::isPresent) to allow partial
assembly; update references to assembledModificationsUuids,
getModificationEntity, modificationRepository.findById and the
assembledModifications list to use this validation.

ModificationGroupEntity originGroup = assembledModifications.stream()
.map(ModificationEntity::getGroup)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
if (originGroup != null) {
List<ModificationEntity> originGroupModifications = originGroup.getModifications();
originGroupModifications.removeIf(mod -> assembledModificationsUuids.contains(mod.getId()));
originGroup.setModifications(originGroupModifications);
assembledModifications.forEach(modificationEntity -> modificationEntity.setGroup(null));
}
// 2. cleans the composites whose submodifications are assembled into a new one
for (ModificationEntity assembledModification : assembledModifications.stream().filter(mod -> mod.getGroup() == null).toList()) {
UUID compositeUuid = modificationRepository.findCompositeIdByContainedModificationId(assembledModification.getId());
if (compositeUuid != null) {
CompositeModificationEntity previousOwner = compositeModificationRepository.findById(compositeUuid).orElse(null);
if (previousOwner != null) {
List<ModificationEntity> modificationsLeft = previousOwner.getModifications()
.stream()
.filter(mod -> !assembledModificationsUuids.contains(mod.getId()))
.toList();
previousOwner.setModifications(modificationsLeft);
}
}
}

// create the new composite
CompositeModificationInfos newCompositeInfos = CompositeModificationInfos.builder()
.modificationsInfos(List.of())
.name("Composite modification")
.build();
CompositeModificationEntity newCompositeEntity = (CompositeModificationEntity) ModificationEntity.fromDTO(newCompositeInfos);
newCompositeEntity.setModificationsOrder(targetIndex);

// assign modifications
newCompositeEntity.setModifications(assembledModifications);
// put the new composite in the target group or composite
if (targetGroup != null) {
List<ModificationEntity> modifications = targetGroup.getModifications();
modifications.add(targetIndex, newCompositeEntity);
targetGroup.setModifications(modifications);
} else if (targetComposite != null) {
List<ModificationEntity> modifications = targetComposite.getModifications();
modifications.add(targetIndex, newCompositeEntity);
for (int i = 0; i < targetComposite.getModifications().size(); i++) {
Comment on lines +917 to +974
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Recompute insertion index after removals to avoid IndexOutOfBoundsException.

targetIndex is captured before detaching/removing assembled items. If items before targetIndex are removed from the same target list, add(targetIndex, ...) can fail or place the composite at the wrong position.

Suggested fix pattern
-        final int targetIndex = firstModificationEntity.getModificationsOrder();
+        final int originalTargetIndex = firstModificationEntity.getModificationsOrder();
...
-            modifications.add(targetIndex, newCompositeEntity);
+            long removedBefore = assembledModifications.stream()
+                    .filter(m -> m.getGroup() != null && m.getGroup().getId().equals(targetGroup.getId()))
+                    .filter(m -> m.getModificationsOrder() < originalTargetIndex)
+                    .count();
+            int insertionIndex = Math.max(0, originalTargetIndex - (int) removedBefore);
+            modifications.add(Math.min(insertionIndex, modifications.size()), newCompositeEntity);

Apply equivalent logic in the targetComposite branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/gridsuite/modification/server/repositories/NetworkModificationRepository.java`
around lines 917 - 974, The code captures targetIndex before removals so calling
modifications.add(targetIndex, newCompositeEntity) can throw IndexOutOfBounds or
insert at wrong place; before inserting into targetGroup.getModifications() or
targetComposite.getModifications() recompute a safe insertion index (e.g., int
insertionIndex = Math.min(targetIndex, modifications.size()); or recalc relative
position from the current list) and use modifications.add(insertionIndex,
newCompositeEntity); update both the targetGroup and targetComposite branches
where targetIndex and newCompositeEntity are used.

targetComposite.getModifications().get(i).setModificationsOrder(i);
}
}

return modificationRepository.save(newCompositeEntity);
}

@Transactional
public void moveSubModification(@NonNull UUID groupUuid, UUID sourceCompositeUuid, UUID targetCompositeUuid,
@NonNull UUID modificationUuid, UUID beforeUuid) {
Expand Down Expand Up @@ -959,10 +1029,6 @@ public void moveSubModification(@NonNull UUID groupUuid, UUID sourceCompositeUui
group.setModifications(notMovedMods);
movedMods.forEach(entity -> entity.setGroup(null));
}
// reordering the composite modifications left (not moved)
for (int i = 0; i < notMovedMods.size(); i++) {
notMovedMods.get(i).setModificationsOrder(i);
}

if (targetCompositeUuid != null) {
// Check if targeted composite isn't already inside modificationUuid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.gridsuite.filter.AbstractFilter;
import org.gridsuite.modification.ModificationType;
import org.gridsuite.modification.NetworkModificationException;
import org.gridsuite.modification.dto.CompositeModificationInfos;
import org.gridsuite.modification.dto.EquipmentModificationInfos;
import org.gridsuite.modification.dto.GenerationDispatchInfos;
import org.gridsuite.modification.dto.ModificationInfos;
Expand Down Expand Up @@ -474,6 +475,14 @@ public CompletableFuture<NetworkModificationsResult> insertCompositeModification
new NetworkModificationsResult(ids, result));
}

@Transactional
public UUID assembleNetworkModificationsIntoNewComposite(@NonNull List<UUID> assembledModificationsUuids) {
CompositeModificationInfos newComposite =
networkModificationRepository.assembleNetworkModificationsIntoNewComposite(assembledModificationsUuids).toModificationInfos();

return newComposite.getUuid();
}

@Transactional
public UUID createNetworkCompositeModification(@NonNull List<UUID> modificationUuids) {
return networkModificationRepository.createNetworkCompositeModification(modificationUuids);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import org.gridsuite.modification.dto.ModificationInfos;
import org.gridsuite.modification.server.dto.NetworkModificationResult;
import org.gridsuite.modification.server.dto.NetworkModificationsResult;
import org.gridsuite.modification.server.entities.CompositeModificationEntity;
import org.gridsuite.modification.server.entities.ModificationEntity;
import org.gridsuite.modification.server.repositories.CompositeModificationRepository;
import org.gridsuite.modification.server.repositories.NetworkModificationRepository;
import org.gridsuite.modification.server.service.ReportService;
import org.gridsuite.modification.server.utils.NetworkCreation;
Expand All @@ -38,11 +40,7 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.*;

import static org.gridsuite.modification.ModificationType.COMPOSITE_MODIFICATION;
import static org.gridsuite.modification.server.utils.NetworkCreation.VARIANT_ID;
Expand Down Expand Up @@ -76,6 +74,9 @@
@Autowired
private NetworkModificationRepository modificationRepository;

@Autowired
private CompositeModificationRepository compositeRepository;

@MockitoBean
private ReportService reportService;

Expand Down Expand Up @@ -276,7 +277,9 @@

Map<UUID, UUID> returnedMap = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });
assertEquals(1, returnedMap.size());
Map.Entry<UUID, UUID> returnedIds = returnedMap.entrySet().stream().findFirst().get();
Optional<Map.Entry<UUID, UUID>> first = returnedMap.entrySet().stream().findFirst();
assertTrue(first.isPresent());
Map.Entry<UUID, UUID> returnedIds = first.get();
UUID returnedSourceId = returnedIds.getKey();
UUID returnedNewId = returnedIds.getValue();
assertNotEquals(returnedSourceId, returnedNewId);
Expand Down Expand Up @@ -484,6 +487,96 @@
assertNull(remainingEntity.getGroup());
}

@Test
void testAssembleNetworkModificationsIntoNewComposite() throws Exception {

Check warning on line 491 in src/test/java/org/gridsuite/modification/server/CompositeControllerTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce the number of assertions from 28 to less than 25.

See more on https://sonarcloud.io/project/issues?id=org.gridsuite%3Anetwork-modification-server&issues=AZ473Nk6vIQMLAbcvvGB&open=AZ473Nk6vIQMLAbcvvGB&pullRequest=810
// Create 3 root-level modifications in the group
List<ModificationInfos> rootMods = createSomeSwitchModifications(TEST_GROUP_ID, 3);
final List<UUID> originalRootModUuids = rootMods.stream().map(ModificationInfos::getUuid).toList();

assertEquals(3, modificationRepository.getModifications(TEST_GROUP_ID, true, true).size());

// ---- 1. Assemble the first 2 root-level modifications into a new composite
List<UUID> assembledModificationUuids = originalRootModUuids.subList(0, 2);
MvcResult mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/")
.content(mapper.writeValueAsString(assembledModificationUuids))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andReturn();

UUID firstCompositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });
assertNotNull(firstCompositeUuid);

// The root group should now contain the new composite and the remaining non-assembled modification
List<ModificationInfos> rootModificationsAfterAssemble = modificationRepository.getModifications(TEST_GROUP_ID, true, true);
assertEquals(2, rootModificationsAfterAssemble.size());
assertEquals(firstCompositeUuid, rootModificationsAfterAssemble.getFirst().getUuid());
assertEquals(COMPOSITE_MODIFICATION, rootModificationsAfterAssemble.getFirst().getType());
assertEquals(originalRootModUuids.get(2), rootModificationsAfterAssemble.get(1).getUuid());

// The new composite should contain the assembled modifications in the same order
Map<UUID, List<ModificationInfos>> compositeContentMap = mapper.readValue(
mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", firstCompositeUuid))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(),
new TypeReference<>() { });
List<ModificationInfos> compositeContent = compositeContentMap.get(firstCompositeUuid);

assertEquals(2, compositeContent.size());
assertEquals(originalRootModUuids.get(0), compositeContent.get(0).getUuid());
assertEquals(originalRootModUuids.get(1), compositeContent.get(1).getUuid());

// The new composite must belong to TEST_GROUP_ID at root level
CompositeModificationEntity firstComposite = compositeRepository.findById(firstCompositeUuid).orElseThrow();
assertNotNull(firstComposite.getGroup());
assertEquals(TEST_GROUP_ID, firstComposite.getGroup().getId());

// The assembled modifications must no longer belong directly to the group
ModificationEntity firstAssembledEntity = modificationRepository.getModificationEntity(originalRootModUuids.get(0));
ModificationEntity secondAssembledEntity = modificationRepository.getModificationEntity(originalRootModUuids.get(1));
assertNull(firstAssembledEntity.getGroup());
assertNull(secondAssembledEntity.getGroup());

// The non-assembled modification must still belong to TEST_GROUP_ID
ModificationEntity remainingInGroupEntity = modificationRepository.getModificationEntity(originalRootModUuids.get(2));
assertNotNull(remainingInGroupEntity.getGroup());
assertEquals(TEST_GROUP_ID, remainingInGroupEntity.getGroup().getId());

// ---- 2. now assembles a modification which is inside a composite with something that is outside :
assembledModificationUuids = List.of(compositeContent.getFirst().getUuid(), remainingInGroupEntity.getId());
mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/")
.content(mapper.writeValueAsString(assembledModificationUuids))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andReturn();

// this new composite will be generated inside the other composite because its first element was inside it
UUID twodepthCompositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { });
assertNotNull(twodepthCompositeUuid);

// The root group should now contain the new composite and nothing else
rootModificationsAfterAssemble = modificationRepository.getModifications(TEST_GROUP_ID, true, true);
assertEquals(1, rootModificationsAfterAssemble.size());

// The first composite should contain the new composite, then the other untouched modification
compositeContentMap = mapper.readValue(
mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", firstCompositeUuid))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(),
new TypeReference<>() { });
compositeContent = compositeContentMap.get(firstCompositeUuid);

assertEquals(2, compositeContent.size());
assertEquals(twodepthCompositeUuid, compositeContent.get(0).getUuid());
assertEquals(originalRootModUuids.get(1), compositeContent.get(1).getUuid());

// The new 2 depth composite must now belong to the first composite, not to a group
CompositeModificationEntity twoDepthComposite = compositeRepository.findById(twodepthCompositeUuid).orElseThrow();
assertNull(twoDepthComposite.getGroup());
compositeContentMap = mapper.readValue(
mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", firstCompositeUuid))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString(),
new TypeReference<>() { });
assertTrue(compositeContentMap.get(firstCompositeUuid).stream()
.map(ModificationInfos::getUuid)
.anyMatch(twodepthCompositeUuid::equals));
}

@Test
void testExpandToLeafUuidsNestedComposites() throws Exception {
// Build nested structure: outerComposite → [innerComposite → [leaf1, leaf2], leaf3]
Expand Down
Loading