Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parent field occasionally missing in /api/v1/project/{uuid} responses #4049

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -217,7 +217,7 @@ public PaginatedResult getProjects(final String name, final boolean excludeInact
*/
@Override
public Project getProject(final String uuid) {
final Project project = getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name());
final Project project = getObjectByUuid(Project.class, UUID.fromString(uuid), List.of(Project.FetchGroup.ALL.name()));
if (project != null) {
// set Metrics to minimize the number of round trips a client needs to make
project.setMetrics(getMostRecentProjectMetrics(project));
Expand Down
14 changes: 8 additions & 6 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1353,15 +1353,17 @@ public PaginatedResult getTags(String policyUuid) {
* @param fetchGroups Fetch groups to use for this operation
* @return The object if found, otherwise {@code null}
* @param <T> Type of the object
* @throws Exception When closing the query failed
* @since 4.6.0
*/
public <T> T getObjectByUuid(final Class<T> clazz, final UUID uuid, final List<String> fetchGroups) throws Exception {
try (final Query<T> query = pm.newQuery(clazz)) {
query.setFilter("uuid == :uuid");
query.setParameters(uuid);
query.getFetchPlan().setGroups(fetchGroups);
public <T> T getObjectByUuid(final Class<T> clazz, final UUID uuid, final List<String> fetchGroups) {
final Query<T> query = pm.newQuery(clazz);
query.setFilter("uuid == :uuid");
query.setParameters(uuid);
query.getFetchPlan().setGroups(fetchGroups);
try {
return query.executeUnique();
} finally {
query.closeAll();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -1078,4 +1081,76 @@ public void cloneProjectWithAclTest() {
Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token")));
}

@Test // https://github.com/DependencyTrack/dependency-track/issues/4048
public void issue4048RegressionTest() {
final int projectsPerLevel = 10;
final int maxDepth = 5;

final Map<Integer, List<UUID>> projectUuidsByLevel = new HashMap<>();

// Create multiple parent-child hierarchies of projects.
for (int i = 0; i < maxDepth; i++) {
final List<UUID> parentUuids = projectUuidsByLevel.get(i - 1);

for (int j = 0; j < projectsPerLevel; j++) {
final UUID parentUuid = i > 0 ? parentUuids.get(j) : null;

final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder()
.add("name", "project-%d-%d".formatted(i, j))
.add("version", "%d.%d".formatted(i, j));
if (parentUuid != null) {
requestBodyBuilder.add("parent", Json.createObjectBuilder()
.add("uuid", parentUuid.toString()));
}

final Response response = jersey.target(V1_PROJECT)
.request()
.header(X_API_KEY, apiKey)
.put(Entity.json(requestBodyBuilder.build().toString()));
assertThat(response.getStatus()).isEqualTo(201);
final JsonObject jsonResponse = parseJsonObject(response);

projectUuidsByLevel.compute(i, (ignored, uuids) -> {
final UUID uuid = UUID.fromString(jsonResponse.getString("uuid"));
if (uuids == null) {
return new ArrayList<>(List.of(uuid));
}

uuids.add(uuid);
return uuids;
});
}
}

// Pick out the UUIDs of projects that should have a parent (i.e. level 1 or above).
final List<UUID> childUuids = projectUuidsByLevel.entrySet().stream()
.filter(entry -> entry.getKey() > 0)
.map(Map.Entry::getValue)
.flatMap(List::stream)
.toList();

// Create a [uuid -> level] mapping for better assertion failure reporting.
final Map<UUID, Integer> levelByChildUuid = projectUuidsByLevel.entrySet().stream()
.filter(entry -> entry.getKey() > 0)
.flatMap(entry -> {
final Integer level = entry.getKey();
return entry.getValue().stream().map(uuid -> Map.entry(uuid, level));
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

// Request all child projects individually.
// Ensure that the parent field is populated for all of them.
for (final UUID uuid : childUuids) {
final Response response = jersey.target(V1_PROJECT + "/" + uuid)
.request()
.header(X_API_KEY, apiKey)
.get();
assertThat(response.getStatus()).isEqualTo(200);
final JsonObject json = parseJsonObject(response);
assertThat(json.getJsonObject("parent"))
.withFailMessage("Parent missing on level: " + levelByChildUuid.get(uuid))
.isNotEmpty();
}
}

}