diff --git a/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java b/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java index eb3f75572..7371db240 100644 --- a/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java +++ b/src/main/java/org/dependencytrack/persistence/jdbi/WorkflowDao.java @@ -185,4 +185,16 @@ HAVING NOW() - MAX("UPDATED_AT") > :retentionDuration """) int deleteAllForRetention(@Bind Duration retentionDuration); + /** + * @since 5.6.0 + */ + @SqlQuery(""" + SELECT EXISTS( + SELECT 1 + FROM "WORKFLOW_STATE" + WHERE "TOKEN" = :token + AND "STATUS" IN ('PENDING', 'TIMED_OUT')) + """) + boolean existsWithNonTerminalStatus(@Bind UUID token); + } diff --git a/src/main/java/org/dependencytrack/resources/v1/EventResource.java b/src/main/java/org/dependencytrack/resources/v1/EventResource.java index df75bf50e..b8cfea6ca 100644 --- a/src/main/java/org/dependencytrack/resources/v1/EventResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/EventResource.java @@ -36,10 +36,13 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.persistence.jdbi.WorkflowDao; import org.dependencytrack.resources.v1.vo.IsTokenBeingProcessedResponse; import java.util.UUID; +import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle; + /** * JAX-RS resources for processing Events * @@ -82,9 +85,18 @@ public class EventResource extends AlpineResource { public Response isTokenBeingProcessed( @Parameter(description = "The UUID of the token to query", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { - final boolean value = Event.isEventBeingProcessed(UUID.fromString(uuid)); - IsTokenBeingProcessedResponse response = new IsTokenBeingProcessedResponse(); - response.setProcessing(value); + final UUID token = UUID.fromString(uuid); + + final boolean isProcessing; + if (Event.isEventBeingProcessed(token)) { + isProcessing = true; + } else { + isProcessing = withJdbiHandle(getAlpineRequest(), handle -> + handle.attach(WorkflowDao.class).existsWithNonTerminalStatus(token)); + } + + final var response = new IsTokenBeingProcessedResponse(); + response.setProcessing(isProcessing); return Response.ok(response).build(); } } \ No newline at end of file diff --git a/src/test/java/org/dependencytrack/ResourceTest.java b/src/test/java/org/dependencytrack/ResourceTest.java index 85358e146..074f90903 100644 --- a/src/test/java/org/dependencytrack/ResourceTest.java +++ b/src/test/java/org/dependencytrack/ResourceTest.java @@ -55,6 +55,7 @@ public abstract class ResourceTest { protected final String V1_CONFIG_PROPERTY = "/v1/configProperty"; protected final String V1_CWE = "/v1/cwe"; protected final String V1_DEPENDENCY = "/v1/dependency"; + protected final String V1_EVENT = "/v1/event"; protected final String V1_FINDING = "/v1/finding"; protected final String V1_LDAP = "/v1/ldap"; protected final String V1_LICENSE = "/v1/license"; diff --git a/src/test/java/org/dependencytrack/resources/v1/EventResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/EventResourceTest.java new file mode 100644 index 000000000..33d1c2d7f --- /dev/null +++ b/src/test/java/org/dependencytrack/resources/v1/EventResourceTest.java @@ -0,0 +1,125 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1; + +import alpine.server.filters.ApiFilter; +import alpine.server.filters.AuthenticationFilter; +import jakarta.ws.rs.core.Response; +import org.apache.http.HttpStatus; +import org.dependencytrack.JerseyTestRule; +import org.dependencytrack.ResourceTest; +import org.dependencytrack.model.WorkflowState; +import org.glassfish.jersey.server.ResourceConfig; +import org.junit.ClassRule; +import org.junit.Test; + +import java.util.Date; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.model.WorkflowStatus.COMPLETED; +import static org.dependencytrack.model.WorkflowStatus.FAILED; +import static org.dependencytrack.model.WorkflowStatus.PENDING; +import static org.dependencytrack.model.WorkflowStep.BOM_CONSUMPTION; +import static org.dependencytrack.model.WorkflowStep.BOM_PROCESSING; + +public class EventResourceTest extends ResourceTest { + + @ClassRule + public static JerseyTestRule jersey = new JerseyTestRule( + new ResourceConfig(EventResource.class) + .register(ApiFilter.class) + .register(AuthenticationFilter.class)); + + @Test + public void isTokenBeingProcessedTrueTest() { + final UUID uuid = UUID.randomUUID(); + final WorkflowState workflowState1 = new WorkflowState(); + workflowState1.setParent(null); + workflowState1.setStep(BOM_CONSUMPTION); + workflowState1.setStatus(COMPLETED); + workflowState1.setToken(uuid); + workflowState1.setUpdatedAt(new Date()); + var workflowState1Persisted = qm.persist(workflowState1); + final WorkflowState workflowState2 = new WorkflowState(); + workflowState2.setParent(workflowState1Persisted); + workflowState2.setStep(BOM_PROCESSING); + workflowState2.setStatus(PENDING); + workflowState2.setToken(uuid); + workflowState2.setUpdatedAt(new Date()); + qm.persist(workflowState2); + + final Response response = jersey.target(V1_EVENT + "/token/" + uuid).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + final String jsonResponse = getPlainTextBody(response); + assertThatJson(jsonResponse).isEqualTo(""" + { + "processing": true + } + """); + } + + @Test + public void isTokenBeingProcessedFalseTest() { + final UUID uuid = UUID.randomUUID(); + final WorkflowState workflowState1 = new WorkflowState(); + workflowState1.setParent(null); + workflowState1.setStep(BOM_CONSUMPTION); + workflowState1.setStatus(COMPLETED); + workflowState1.setToken(uuid); + workflowState1.setUpdatedAt(new Date()); + var workflowState1Persisted = qm.persist(workflowState1); + final WorkflowState workflowState2 = new WorkflowState(); + workflowState2.setParent(workflowState1Persisted); + workflowState2.setStep(BOM_PROCESSING); + workflowState2.setStatus(FAILED); + workflowState2.setToken(uuid); + workflowState2.setUpdatedAt(new Date()); + qm.persist(workflowState2); + + final Response response = jersey.target(V1_EVENT + "/token/" + uuid).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + final String jsonResponse = getPlainTextBody(response); + assertThatJson(jsonResponse).isEqualTo(""" + { + "processing": false + } + """); + } + + @Test + public void isTokenBeingProcessedNotExistsTest() { + final Response response = jersey.target(V1_EVENT + "/token/" + UUID.randomUUID()).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + final String jsonResponse = getPlainTextBody(response); + assertThatJson(jsonResponse).isEqualTo(""" + { + "processing": false + } + """); + } + +} \ No newline at end of file