Skip to content

Commit 6f830f0

Browse files
fix: dispatch metrics update for collection parent on bulk project delete
When child projects are deleted via deleteProjectsByUUIDs, their parent collection project's metrics are now recalculated. Previously the stale aggregated metrics (vuln counts, risk scores, etc.) would persist on the collection project indefinitely after all children were removed. The single-delete path (recursivelyDelete) already handled this correctly; this aligns the bulk-delete path with the same behaviour. Signed-off-by: Valentijn Scholten <valentijnscholten@gmail.com>
1 parent d0078ec commit 6f830f0

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,16 @@ public void deleteProjectsByUUIDs(Collection<UUID> uuids) {
10191019
throw ProjectOperationException.forDeletion(errorByUUID);
10201020
}
10211021

1022+
// Collect parent collection projects that need metrics updates after deletion.
1023+
// Exclude parents that are themselves being deleted.
1024+
final Set<UUID> collectionParentUuids = projects.stream()
1025+
.map(Project::getParent)
1026+
.filter(parent -> parent != null
1027+
&& parent.getCollectionLogic() != ProjectCollectionLogic.NONE
1028+
&& uuids.stream().noneMatch(u -> u.equals(parent.getUuid())))
1029+
.map(Project::getUuid)
1030+
.collect(Collectors.toSet());
1031+
10221032
Long[] projectIDsArray = accessibleProjectIds.toArray(Long[]::new);
10231033
String commaSeparatedProjectIDs = accessibleProjectIds.stream().map(String::valueOf).collect(Collectors.joining(","));
10241034
var queryParameter = DbUtil.isMssql() ? commaSeparatedProjectIDs : projectIDsArray;
@@ -1325,6 +1335,10 @@ WHERE PROJECT.ID IN (SELECT value FROM STRING_SPLIT(?, ','))
13251335
executeAndCloseWithArray(sqlQuery, queryParameter);
13261336
}
13271337
});
1338+
1339+
for (final UUID parentUuid : collectionParentUuids) {
1340+
Event.dispatch(new ProjectMetricsUpdateEvent(parentUuid));
1341+
}
13281342
}
13291343

13301344
/**

src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232

3333
import java.util.Date;
3434
import java.util.List;
35+
import java.util.UUID;
3536

37+
import static org.assertj.core.api.Assertions.assertThat;
3638
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3739
import static org.mockito.Mockito.times;
3840

@@ -132,4 +134,42 @@ public void testCloneProjectMetricUpdate() throws Exception {
132134
}
133135
}
134136

137+
@Test
138+
void testDeleteProjectsByUUIDsDispatchesMetricsUpdateForCollectionParent() {
139+
final Project collectionParent = qm.createProject("Collection", null, "1.0", null, null, null, true, false);
140+
final Project detachedParent = qm.detach(Project.class, collectionParent.getId());
141+
detachedParent.setCollectionLogic(ProjectCollectionLogic.AGGREGATE_DIRECT_CHILDREN);
142+
qm.updateProject(detachedParent, false);
143+
144+
final Project child = qm.createProject("Child", null, "1.0", null, collectionParent, null, true, false);
145+
146+
try (MockedStatic<Event> mockedEvent = Mockito.mockStatic(Event.class)) {
147+
qm.deleteProjectsByUUIDs(List.of(child.getUuid()));
148+
149+
final ArgumentCaptor<Event> eventCaptor = ArgumentCaptor.forClass(Event.class);
150+
mockedEvent.verify(() -> Event.dispatch(eventCaptor.capture()), times(1));
151+
152+
final List<UUID> dispatchedUuids = eventCaptor.getAllValues().stream()
153+
.filter(e -> e instanceof ProjectMetricsUpdateEvent)
154+
.map(e -> ((ProjectMetricsUpdateEvent) e).getUuid())
155+
.toList();
156+
assertThat(dispatchedUuids).containsExactly(collectionParent.getUuid());
157+
}
158+
}
159+
160+
@Test
161+
void testDeleteProjectsByUUIDsDoesNotDispatchMetricsUpdateWhenParentAlsoDeleted() {
162+
final Project collectionParent = qm.createProject("Collection", null, "1.0", null, null, null, true, false);
163+
final Project detachedParent = qm.detach(Project.class, collectionParent.getId());
164+
detachedParent.setCollectionLogic(ProjectCollectionLogic.AGGREGATE_DIRECT_CHILDREN);
165+
qm.updateProject(detachedParent, false);
166+
167+
final Project child = qm.createProject("Child", null, "1.0", null, collectionParent, null, true, false);
168+
169+
try (MockedStatic<Event> mockedEvent = Mockito.mockStatic(Event.class)) {
170+
qm.deleteProjectsByUUIDs(List.of(collectionParent.getUuid(), child.getUuid()));
171+
172+
mockedEvent.verify(() -> Event.dispatch(Mockito.any(ProjectMetricsUpdateEvent.class)), times(0));
173+
}
174+
}
135175
}

0 commit comments

Comments
 (0)