From 652ad3d86704818cb4e85405018a7491190cc468 Mon Sep 17 00:00:00 2001 From: Fabrizzio Araya <37148755+fabrizzio-dotCMS@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:59:38 -0600 Subject: [PATCH] fix(JobQueue) Fix on the info retrieved by the status endpoint Refs: #29480 (#30386) ### Proposed Changes * status endpoint now includes all the details of the job * Improvements on the postmans --- .../jobs/business/error/CircuitBreaker.java | 2 +- .../business/error/RetryStrategyProducer.java | 2 +- .../dotcms/jobs/business/job/AbstractJob.java | 2 + .../rest/api/v1/job/JobQueueResource.java | 35 +- ...ueResourceAPITests.postman_collection.json | 667 ++++++++++++++---- 5 files changed, 551 insertions(+), 157 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/jobs/business/error/CircuitBreaker.java b/dotCMS/src/main/java/com/dotcms/jobs/business/error/CircuitBreaker.java index ee6b0d3147b7..fcaa3f65f782 100644 --- a/dotCMS/src/main/java/com/dotcms/jobs/business/error/CircuitBreaker.java +++ b/dotCMS/src/main/java/com/dotcms/jobs/business/error/CircuitBreaker.java @@ -16,7 +16,7 @@ public class CircuitBreaker { // The number of failures that will cause the circuit to open static final int DEFAULT_CIRCUIT_BREAKER_FAILURE_THRESHOLD = Config.getIntProperty( - "DEFAULT_CIRCUIT_BREAKER_FAILURE_THRESHOLD", 5 + "DEFAULT_CIRCUIT_BREAKER_FAILURE_THRESHOLD", 10 ); // The time in milliseconds after which to attempt to close the circuit diff --git a/dotCMS/src/main/java/com/dotcms/jobs/business/error/RetryStrategyProducer.java b/dotCMS/src/main/java/com/dotcms/jobs/business/error/RetryStrategyProducer.java index ca3b48b8fb7e..2b9452d66713 100644 --- a/dotCMS/src/main/java/com/dotcms/jobs/business/error/RetryStrategyProducer.java +++ b/dotCMS/src/main/java/com/dotcms/jobs/business/error/RetryStrategyProducer.java @@ -28,7 +28,7 @@ public class RetryStrategyProducer { // The maximum number of retry attempts allowed static final int DEFAULT_RETRY_STRATEGY_MAX_RETRIES = Config.getIntProperty( - "DEFAULT_RETRY_STRATEGY_MAX_RETRIES", 3 + "DEFAULT_RETRY_STRATEGY_MAX_RETRIES", 1 ); /** diff --git a/dotCMS/src/main/java/com/dotcms/jobs/business/job/AbstractJob.java b/dotCMS/src/main/java/com/dotcms/jobs/business/job/AbstractJob.java index 6af21ec986e0..590458a07f2f 100644 --- a/dotCMS/src/main/java/com/dotcms/jobs/business/job/AbstractJob.java +++ b/dotCMS/src/main/java/com/dotcms/jobs/business/job/AbstractJob.java @@ -2,6 +2,7 @@ import com.dotcms.jobs.business.processor.ProgressTracker; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.time.LocalDateTime; @@ -47,6 +48,7 @@ public interface AbstractJob { Map parameters(); + @JsonIgnore Optional progressTracker(); @Default diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueResource.java index 109807d7a1ab..fc15b93c9714 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueResource.java @@ -13,6 +13,7 @@ import graphql.VisibleForTesting; import java.io.IOException; import java.util.Map; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; @@ -68,21 +69,20 @@ public Response createJob( @GET @Path("/queues") @Produces(MediaType.APPLICATION_JSON) - public Response getQueues(@Context HttpServletRequest request) { + public ResponseEntityView> getQueues(@Context HttpServletRequest request) { new WebResource.InitBuilder(webResource) .requiredBackendUser(true) .requiredFrontendUser(false) .requestAndResponse(request, null) .rejectWhenNoUser(true) .init(); - return Response.ok(new ResponseEntityView<>(helper.getQueueNames())).build(); - + return new ResponseEntityView<>(helper.getQueueNames()); } @GET @Path("/{jobId}/status") @Produces(MediaType.APPLICATION_JSON) - public Response getJobStatus(@Context HttpServletRequest request, @PathParam("jobId") String jobId) + public ResponseEntityView getJobStatus(@Context HttpServletRequest request, @PathParam("jobId") String jobId) throws DotDataException { new WebResource.InitBuilder(webResource) @@ -93,21 +93,14 @@ public Response getJobStatus(@Context HttpServletRequest request, @PathParam("jo .init(); Job job = helper.getJob(jobId); - Map statusInfo = Map.of( - "state", job.state(), - "progress", job.progress(), - "executionNode", job.executionNode().orElse("N/A") - ); - - return Response.ok(new ResponseEntityView<>(statusInfo)).build(); - + return new ResponseEntityView<>(job); } @POST @Path("/{jobId}/cancel") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.WILDCARD) - public Response cancelJob(@Context HttpServletRequest request, @PathParam("jobId") String jobId) + public ResponseEntityView cancelJob(@Context HttpServletRequest request, @PathParam("jobId") String jobId) throws DotDataException { new WebResource.InitBuilder(webResource) .requiredBackendUser(true) @@ -116,12 +109,12 @@ public Response cancelJob(@Context HttpServletRequest request, @PathParam("jobId .rejectWhenNoUser(true) .init(); helper.cancelJob(jobId); - return Response.ok(new ResponseEntityView<>("Job cancelled successfully")).build(); + return new ResponseEntityView<>("Job cancelled successfully"); } @GET @Produces(MediaType.APPLICATION_JSON) - public Response listJobs(@Context HttpServletRequest request, + public ResponseEntityView listJobs(@Context HttpServletRequest request, @QueryParam("page") @DefaultValue("1") int page, @QueryParam("pageSize") @DefaultValue("20") int pageSize) { new WebResource.InitBuilder(webResource) @@ -131,14 +124,13 @@ public Response listJobs(@Context HttpServletRequest request, .rejectWhenNoUser(true) .init(); final JobPaginatedResult result = helper.getJobs(page, pageSize); - return Response.ok(new ResponseEntityView<>(result)).build(); - + return new ResponseEntityView<>(result); } @GET @Path("/{queueName}/active") @Produces(MediaType.APPLICATION_JSON) - public Response activeJobs(@Context HttpServletRequest request, @PathParam("queueName") String queueName, + public ResponseEntityView activeJobs(@Context HttpServletRequest request, @PathParam("queueName") String queueName, @QueryParam("page") @DefaultValue("1") int page, @QueryParam("pageSize") @DefaultValue("20") int pageSize) { new WebResource.InitBuilder(webResource) @@ -148,14 +140,13 @@ public Response activeJobs(@Context HttpServletRequest request, @PathParam("queu .rejectWhenNoUser(true) .init(); final JobPaginatedResult result = helper.getActiveJobs(queueName, page, pageSize); - return Response.ok(new ResponseEntityView<>(result)).build(); - + return new ResponseEntityView<>(result); } @GET @Path("/failed") @Produces(MediaType.APPLICATION_JSON) - public Response failedJobs(@Context HttpServletRequest request, + public ResponseEntityView failedJobs(@Context HttpServletRequest request, @QueryParam("page") @DefaultValue("1") int page, @QueryParam("pageSize") @DefaultValue("20") int pageSize) { new WebResource.InitBuilder(webResource) @@ -165,7 +156,7 @@ public Response failedJobs(@Context HttpServletRequest request, .rejectWhenNoUser(true) .init(); final JobPaginatedResult result = helper.getFailedJobs(page, pageSize); - return Response.ok(new ResponseEntityView<>(result)).build(); + return new ResponseEntityView<>(result); } diff --git a/dotcms-postman/src/main/resources/postman/JobQueueResourceAPITests.postman_collection.json b/dotcms-postman/src/main/resources/postman/JobQueueResourceAPITests.postman_collection.json index d1922eb66eed..f7aa1934dadc 100644 --- a/dotcms-postman/src/main/resources/postman/JobQueueResourceAPITests.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/JobQueueResourceAPITests.postman_collection.json @@ -1,10 +1,11 @@ { "info": { - "_postman_id": "3b8039cc-b927-45f0-a199-04c71e8d8fcf", + "_postman_id": "a12c5acf-e63e-4357-9642-07ca2795b509", "name": "JobQueueResource API Tests", "description": "Postman collection for testing the JobQueueResource API endpoints.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "10041132" + "_exporter_id": "10041132", + "_collection_link": "https://speeding-firefly-555540.postman.co/workspace/blank~a8ffdb2b-2b56-46fa-ae3e-f4b3b0f8204a/collection/10041132-a12c5acf-e63e-4357-9642-07ca2795b509?action=share&source=collection_link&creator=10041132" }, "item": [ { @@ -175,7 +176,9 @@ "var jsonData = pm.response.json();", "pm.expect(jsonData.entity).to.be.a('String');", "// Save jobId to environment variable", - "pm.environment.set(\"jobId\", jsonData.entity);" + "pm.collectionVariables.set(\"jobId\", jsonData.entity);", + "let jId = pm.collectionVariables.get(\"jobId\");", + "console.log(jId);" ], "type": "text/javascript", "packages": {} @@ -232,19 +235,120 @@ " pm.response.to.have.status(200);", "});", "", - "// Check if status info is returned", - "var jsonData = pm.response.json();", - "pm.test(\"Response has status info\", function () {", - " pm.expect(jsonData.entity).to.be.an('object');", - " pm.expect(jsonData.entity).to.have.property('state');", - " pm.expect(jsonData.entity).to.have.property('progress'); ", + "", + "// Store the response in a variable", + "let response = pm.response.json();", + "", + "// Validate that the response status is 200 OK", + "pm.test(\"Response status is 200\", function () {", + " pm.response.to.have.status(200);", "});", "", - "pm.test(\"Job is RUNNING\", function () {", - " var object = jsonData.entity;", - " pm.expect(object.state).to.be.oneOf([\"RUNNING\", \"PENDING\"]); ", - " ", - "});" + "// Check if the 'entity' object exists", + "pm.test(\"'entity' object exists\", function () {", + " pm.expect(response).to.have.property(\"entity\");", + "});", + "", + "// Validate specific fields within `entity`", + "let entity = response.entity;", + "", + "// Check if 'completedAt' is either null or a valid date string", + "pm.test(\"'completedAt' is null or a valid date\", function () {", + " pm.expect(entity.completedAt).to.satisfy(function(val) {", + " return val === null || new Date(val).toString() !== \"Invalid Date\";", + " });", + "});", + "", + "// Check if 'createdAt' is a valid date string", + "pm.test(\"'createdAt' is a valid date string\", function () {", + " pm.expect(entity.createdAt).to.be.a(\"string\");", + " pm.expect(new Date(entity.createdAt)).to.not.equal(\"Invalid Date\");", + "});", + "", + "// Check if 'executionNode' is a UUID", + "pm.test(\"'executionNode' is a valid UUID\", function () {", + " pm.expect(entity.executionNode).to.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);", + "});", + "", + "// Check if 'id' is a UUID", + "pm.test(\"'id' is a valid UUID\", function () {", + " pm.expect(entity.id).to.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);", + "});", + "", + "// Validate the inner `parameters` object", + "let parameters = entity.parameters;", + "", + "pm.test(\"'parameters' contains expected keys with valid values\", function () {", + " pm.expect(parameters).to.have.property(\"nLines\").that.is.a(\"string\");", + " pm.expect(parameters).to.have.property(\"requestFingerPrint\").that.is.a(\"string\");", + " pm.expect(parameters.requestFingerPrint).to.have.lengthOf(44); // Base64 strings are often 44 characters for SHA-256", + " pm.expect(parameters).to.have.property(\"tempFileId\").that.is.a(\"string\");", + "});", + "", + "// Check if 'progress' is a number between 0 and 1 (e.g., a percentage)", + "pm.test(\"'progress' is a number between 0 and 1\", function () {", + " pm.expect(entity.progress).to.be.a(\"number\").within(0, 1);", + "});", + "", + "// Check if 'queueName' is a non-empty string", + "pm.test(\"'queueName' is a non-empty string\", function () {", + " pm.expect(entity.queueName).to.be.a(\"string\").that.is.not.empty;", + "});", + "", + "// Check if 'result' is either null or a valid JSON object", + "pm.test(\"'result' is null or an object\", function () {", + " pm.expect(entity.result === null || typeof entity.result === \"object\").to.be.true;", + "});", + "", + "// Check if 'retryCount' is a non-negative integer", + "pm.test(\"'retryCount' is a non-negative integer\", function () {", + " pm.expect(entity.retryCount).to.be.a(\"number\").that.is.at.least(0);", + "});", + "", + "// Check if 'startedAt' is either null or a valid date", + "pm.test(\"'startedAt' is null or a valid date\", function () {", + " pm.expect(entity.startedAt).to.satisfy(function(val) {", + " return val === null || new Date(val).toString() !== \"Invalid Date\";", + " });", + "});", + "", + "// Check if 'state' is a non-empty string", + "pm.test(\"'state' is a non-empty string\", function () {", + " pm.expect(entity.state).to.be.a(\"string\").that.is.not.empty;", + "});", + "", + "//Check status is valid ", + "pm.test(\"Job is RUNNING or PENDING\", function () { ", + " pm.expect(entity.state).to.be.oneOf([\"RUNNING\", \"PENDING\"]); ", + "});", + "", + "// Check if 'updatedAt' is a valid date string", + "pm.test(\"'updatedAt' is a valid date string\", function () {", + " pm.expect(entity.updatedAt).to.be.a(\"string\");", + " pm.expect(new Date(entity.updatedAt)).to.not.equal(\"Invalid Date\");", + "});", + "", + "// Validate other top-level objects in the response", + "pm.test(\"'errors' is an empty array\", function () {", + " pm.expect(response.errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "pm.test(\"'i18nMessagesMap' is an empty object\", function () {", + " pm.expect(response.i18nMessagesMap).to.be.an(\"object\").that.is.empty;", + "});", + "", + "pm.test(\"'messages' is an empty array\", function () {", + " pm.expect(response.messages).to.be.an(\"array\").that.is.empty;", + "});", + "", + "pm.test(\"'pagination' is null\", function () {", + " pm.expect(response.pagination).to.be.null;", + "});", + "", + "pm.test(\"'permissions' is an empty array\", function () {", + " pm.expect(response.permissions).to.be.an(\"array\").that.is.empty;", + "});", + "" ], "type": "text/javascript", "packages": {} @@ -288,8 +392,19 @@ " pm.expect(jsonData.entity).to.equal('Job cancelled successfully');", "});", "", - "var jobId = pm.environment.get(\"jobId\");", - "pm.environment.set(\"cancelledJobId\",jobId);" + "var jobId = pm.collectionVariables.get(\"jobId\");", + "console.log(\" At the time this request was sent \" + jobId);", + "pm.collectionVariables.set(\"cancelledJobId\",jobId);" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" ], "type": "text/javascript", "packages": {} @@ -330,7 +445,7 @@ "var jsonData = pm.response.json();", "pm.expect(jsonData.entity).to.be.a('String');", "// Save jobId to environment variable", - "pm.environment.set(\"jobId\", jsonData.entity);" + "pm.collectionVariables.set(\"jobId\", jsonData.entity);" ], "type": "text/javascript", "packages": {} @@ -383,24 +498,95 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 200\", function () {", + "", + "// Store the response in a variable", + "let response = pm.response.json();", + "", + "// Validate that the response status is 200 OK", + "pm.test(\"Response status is 200\", function () {", " pm.response.to.have.status(200);", "});", "", - "// Check if active jobs are returned", - "var jsonData = pm.response.json();", - "pm.test(\"Response has active jobs list\", function () {", - " pm.expect(jsonData.entity).to.have.property('jobs');", - " pm.expect(jsonData.entity.jobs).to.be.an('array');", + "// Check if the 'entity' object exists", + "pm.test(\"'entity' object exists\", function () {", + " pm.expect(response).to.have.property(\"entity\");", "});", "", - "var jobsArray = jsonData.entity.jobs;", + "// Validate the fields within `entity`", + "let entity = response.entity;", "", - "pm.test(\"jobsArray contains objects\", function () {", - " pm.expect(jobsArray[0]).to.be.an('object');", + "// Check that 'jobs' is an array and validate its length", + "pm.test(\"'jobs' is an array with the correct length\", function () {", + " pm.expect(entity).to.have.property(\"jobs\").that.is.an(\"array\").with.lengthOf(entity.total);", "});", "", - "var jobId = pm.environment.get(\"jobId\");", + "// Iterate over each job in the 'jobs' array", + "entity.jobs.forEach((job, index) => {", + " pm.test(`Job ${index + 1}: 'completedAt' is null or a valid date`, function () {", + " pm.expect(job.completedAt).to.satisfy(function(val) {", + " return val === null || new Date(val).toString() !== \"Invalid Date\";", + " });", + " });", + "", + " pm.test(`Job ${index + 1}: 'createdAt' is a valid date string`, function () {", + " pm.expect(job.createdAt).to.be.a(\"string\");", + " pm.expect(new Date(job.createdAt)).to.not.equal(\"Invalid Date\");", + " });", + "", + " pm.test(`Job ${index + 1}: 'executionNode' is a valid UUID`, function () {", + " pm.expect(job.executionNode).to.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);", + " });", + "", + " pm.test(`Job ${index + 1}: 'id' is a valid UUID`, function () {", + " pm.expect(job.id).to.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);", + " });", + "", + " // Validate the `parameters` object", + " let parameters = job.parameters;", + "", + " pm.test(`Job ${index + 1}: 'parameters' contains expected keys with valid values`, function () {", + " pm.expect(parameters).to.have.property(\"nLines\").that.is.a(\"string\");", + " pm.expect(parameters).to.have.property(\"requestFingerPrint\").that.is.a(\"string\");", + " pm.expect(parameters.requestFingerPrint).to.have.lengthOf(44); // Typical length for SHA-256 in Base64", + " pm.expect(parameters).to.have.property(\"tempFileId\").that.is.a(\"string\");", + " });", + "", + " pm.test(`Job ${index + 1}: 'progress' is a number between 0 and 1`, function () {", + " pm.expect(job.progress).to.be.a(\"number\").within(0, 1);", + " });", + "", + " pm.test(`Job ${index + 1}: 'queueName' is a non-empty string`, function () {", + " pm.expect(job.queueName).to.be.a(\"string\").that.is.not.empty;", + " });", + "", + " pm.test(`Job ${index + 1}: 'result' is null or an object`, function () {", + " pm.expect(job.result === null || typeof job.result === \"object\").to.be.true;", + " });", + "", + " pm.test(`Job ${index + 1}: 'retryCount' is a non-negative integer`, function () {", + " pm.expect(job.retryCount).to.be.a(\"number\").that.is.at.least(0);", + " });", + "", + " pm.test(`Job ${index + 1}: 'startedAt' is null or a valid date`, function () {", + " pm.expect(job.startedAt).to.satisfy(function(val) {", + " return val === null || new Date(val).toString() !== \"Invalid Date\";", + " });", + " });", + "", + " pm.test(`Job ${index + 1}: 'state' is a non-empty string`, function () {", + " pm.expect(job.state).to.be.a(\"string\").that.is.not.empty;", + " });", + "", + " pm.test(`Job ${index + 1}: 'updatedAt' is a valid date string`, function () {", + " pm.expect(job.updatedAt).to.be.a(\"string\");", + " pm.expect(new Date(job.updatedAt)).to.not.equal(\"Invalid Date\");", + " });", + "});", + "", + "//Look for the last created job ", + "let jobsArray = entity.jobs;", + "", + "var jobId = pm.collectionVariables.get(\"jobId\");", "pm.test(\"jobId is present in the response\", function () {", " var jobFound = jobsArray.some(function(job) {", " return job.id === jobId;", @@ -408,6 +594,39 @@ " pm.expect(jobFound).to.be.true;", "});", "", + "// Validate pagination fields within `entity`", + "pm.test(\"'page' is a positive integer\", function () {", + " pm.expect(entity.page).to.be.a(\"number\").that.is.at.least(1);", + "});", + "", + "pm.test(\"'pageSize' is a positive integer\", function () {", + " pm.expect(entity.pageSize).to.be.a(\"number\").that.is.at.least(1);", + "});", + "", + "pm.test(\"'total' matches the length of 'jobs' array\", function () {", + " pm.expect(entity.total).to.equal(entity.jobs.length);", + "});", + "", + "// Validate other top-level objects in the response", + "pm.test(\"'errors' is an empty array\", function () {", + " pm.expect(response.errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "pm.test(\"'i18nMessagesMap' is an empty object\", function () {", + " pm.expect(response.i18nMessagesMap).to.be.an(\"object\").that.is.empty;", + "});", + "", + "pm.test(\"'messages' is an empty array\", function () {", + " pm.expect(response.messages).to.be.an(\"array\").that.is.empty;", + "});", + "", + "pm.test(\"'pagination' is null\", function () {", + " pm.expect(response.pagination).to.be.null;", + "});", + "", + "pm.test(\"'permissions' is an empty array\", function () {", + " pm.expect(response.permissions).to.be.an(\"array\").that.is.empty;", + "});", "" ], "type": "text/javascript", @@ -462,18 +681,6 @@ "pm.expect(jsonData.entity).to.be.a('String');", "// Save jobId to environment variable", "pm.environment.set(\"failingJobId\", jsonData.entity);", - "", - "function delay(milliseconds) {", - " var start = new Date().getTime();", - " var end = start;", - " while (end - start < milliseconds) {", - " end = new Date().getTime(); ", - " }", - "}", - "", - "delay(8000); ", - "", - "//And now lets wait for a show while ", "" ], "type": "text/javascript", @@ -495,11 +702,12 @@ { "key": "file", "type": "file", - "src": "resources/JobQueue/odyssey.txt" + "src": [], + "disabled": true }, { "key": "params", - "value": "{\n \"nLines\":\"1\"\n}", + "value": "{\n\n}", "type": "text" } ] @@ -575,23 +783,102 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});", + "// Configuration", + "const maxRetries = 5; // Number of times to retry", + "const retryDelay = 4000; // Delay between retries in milliseconds", "", - "// Check if status info is returned", - "var jsonData = pm.response.json();", - "pm.test(\"Response has status info\", function () {", - " pm.expect(jsonData.entity).to.be.an('object');", - " pm.expect(jsonData.entity).to.have.property('state');", - " pm.expect(jsonData.entity).to.have.property('progress'); ", - "});", + "// Get server URL from environment variable", + "const serverURL = pm.environment.get('serverURL') || pm.collectionVariables.get('baseUrl'); // fallback to baseURL if serverURL is not defined", "", - "pm.test(\"Job is RUNNING\", function () {", - " var object = jsonData.entity;", - " pm.expect(object.state).to.be.eql(\"CANCELED\"); ", - " ", - "});" + "// Assuming 'jobId' is set as an environment variable", + "const jobId = pm.collectionVariables.get('cancelledJobId');", + "const checkUrl = `${serverURL}/api/v1/jobs/${jobId}/status`;", + "const cancelUrl = `${serverURL}/api/v1/jobs/${jobId}/cancel`;", + "", + "// Function to check the job state", + "function checkJobState(retriesLeft) {", + "", + " console.log(\"checkURL url :: \"+checkUrl); ", + " console.log(\"cancelUrl url :: \"+cancelUrl); ", + " const token = pm.collectionVariables.get('jwt');", + "", + " pm.sendRequest({", + " url:checkUrl,", + " method:'GET',", + " header: {", + " 'Authorization': `Bearer ${token}`, // Add Bearer token in Authorization header", + " 'Content-Type': 'application/json'", + " }", + " }, ", + " function (err, response) {", + " if (err) {", + " console.error(\"Error retrieving job status:\", err);", + " return;", + " }", + " ", + " let jsonData = response.json();", + " const jobState = jsonData.entity.state;", + " console.log(jobState);", + " ", + " // Test for \"CANCELED\" state", + " if (jobState === \"CANCELED\") {", + " pm.test(\"Job has been CANCELED\", function () {", + " pm.expect(jobState).to.eql(\"CANCELED\");", + " });", + " console.log(\"Job has been successfully canceled.\");", + " } else if (retriesLeft > 0) {", + " console.log(\" retriesLeft :: \"+retriesLeft);", + " // Send a cancel POST request and retry", + " pm.sendRequest({", + " url: cancelUrl,", + " method: 'POST',", + " header: {", + " 'Authorization': `Bearer ${token}`, // Add Bearer token in Authorization header", + " 'Content-Type': 'application/json'", + " }", + " }, function (cancelErr, cancelResponse) {", + " if (cancelErr) {", + " console.error(\"Error sending cancel request:\", cancelErr);", + " } else {", + " console.log(`Cancel request sent. Status: ${cancelResponse.status}`);", + " }", + " ", + " // Wait for a delay and then check the status again", + " setTimeout(function () {", + " checkJobState(retriesLeft - 1);", + " }, retryDelay);", + " });", + " } else {", + " // If maximum retries are reached and job is still not canceled", + " pm.test(\"Job has not been CANCELED after maximum retries\", function () {", + " pm.expect(jobState).to.eql(\"CANCELED\");", + " });", + " console.warn(\"Job status is still not 'CANCELED' after maximum retries.\");", + " }", + " });", + "}", + "", + "// Initial job state check", + "checkJobState(maxRetries);", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "function sleep(milliseconds) {", + " const start = Date.now();", + " while (Date.now() - start < milliseconds) {", + " // Busy-wait loop that blocks the execution", + " }", + "}", + "", + "", + "sleep(9000);" ], "type": "text/javascript", "packages": {} @@ -625,42 +912,73 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});", + "// Configuration", + "const maxRetries = 5; // Maximum number of retries", + "const retryDelay = 2000; // Delay between retries in milliseconds", "", - "// Check if failed jobs are returned", - "var jsonData = pm.response.json();", - "pm.test(\"Response has failed jobs list\", function () {", - " pm.expect(jsonData.entity).to.have.property('jobs');", - " pm.expect(jsonData.entity.jobs).to.be.an('array');", - " pm.expect(jsonData.entity.jobs[0]).to.be.an('object'); ", - "});", + "// Get server URL from environment variables", + "const serverURL = pm.environment.get('serverURL') || pm.collectionVariables.get('baseUrl'); // Fallback to baseURL if serverURL is not defined", "", - "var failingJobId = pm.environment.get(\"failingJobId\"); ", - "pm.test(\"Response has failed matching objects\", function () { ", - " var jobs = jsonData.entity.jobs;", - " var failingJobId = pm.environment.get(\"failingJobId\");", - " ", - " // Filter ", - " var matchingJobs = jobs.filter(function(job) {", - " return job.id === failingJobId;", - " });", - " ", - " // Verify", - " pm.expect(matchingJobs.length).to.be.above(0);", - " ", - " // Assert", - " matchingJobs.forEach(function(job) {", - " pm.expect(job.id).to.equal(failingJobId);", - " pm.expect(job.queueName).to.equal('fail');", - " pm.expect(job.result).to.have.property('errorDetail');", - " pm.expect(job.result.errorDetail).to.have.property('exceptionClass');", - " pm.expect(job.result.errorDetail).to.have.property('message');", - " pm.expect(job.result.errorDetail).to.have.property('stackTrace');", - " pm.expect(job.result.errorDetail).to.have.property('timestamp');", + "// Define the URL for job status verification", + "const checkUrl = `${serverURL}/api/v1/jobs/failed`;", + "", + "// Function to check the status of jobs", + "function checkJobState(retriesLeft) {", + "", + " console.log(\"Checking jobs URL: \" + checkUrl);", + " const token = pm.collectionVariables.get('jwt'); ", + " // Send a GET request to fetch job statuses", + " pm.sendRequest({", + " url: checkUrl,", + " method: 'GET',", + " header: {", + " 'Authorization': `Bearer ${token}`, // Add Bearer token in Authorization header", + " 'Content-Type': 'application/json'", + " }", + " }, function (err, response) {", + " if (err) {", + " console.error(\"Error retrieving job statuses:\", err);", + " return;", + " }", + "", + " let jsonData = response.json();", + " let jobs = jsonData.entity.jobs;", + "", + " if (jobs.length > 0) {", + " // Check if all jobs have the \"FAILED\" status", + " const allFailed = jobs.every(job => job.state === \"FAILED\");", + "", + " if (allFailed) {", + " // Postman test to validate that all jobs are in the \"FAILED\" state", + " pm.test(\"All jobs are in 'FAILED' state\", function () {", + " pm.expect(allFailed).to.be.true;", + " });", + " console.log(\"All jobs are in 'FAILED' state.\");", + " } else {", + " // If any job is not in the \"FAILED\" state", + " pm.test(\"Some jobs are not in 'FAILED' state\", function () {", + " pm.expect(allFailed).to.be.true; // This will fail if not all jobs are \"FAILED\"", + " });", + " console.warn(\"Not all jobs are in 'FAILED' state.\");", + " }", + " } else if (retriesLeft > 0) {", + " // If no jobs are found and retries are left, wait and retry", + " console.log(\"No jobs available, retries left: \" + retriesLeft);", + " setTimeout(function () {", + " checkJobState(retriesLeft - 1);", + " }, retryDelay);", + " } else {", + " // If no jobs and no retries are left", + " pm.test(\"Maximum retries reached, no jobs received.\", function () {", + " pm.expect(jobs.length).to.be.greaterThan(0); // This will fail if no jobs are found", + " });", + " console.warn(\"No jobs found after maximum retries.\");", + " }", " });", - "});", + "}", + "", + "// Start job status check with the maximum number of retries", + "checkJobState(maxRetries);", "" ], "type": "text/javascript", @@ -706,46 +1024,77 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});", + "// Configuration", + "const maxRetries = 5; // Maximum number of retries", + "const retryDelay = 2000; // Delay between retries in milliseconds", "", - "// Check if jobs are returned", - "var jsonData = pm.response.json();", - "pm.test(\"Response has jobs list\", function () {", - " pm.expect(jsonData.entity).to.have.property('jobs');", - " pm.expect(jsonData.entity.jobs).to.be.an('array'); ", - "});", + "// Get server URL from environment variables", + "const serverURL = pm.environment.get('serverURL') || pm.collectionVariables.get('baseUrl'); // Use baseURL as fallback if serverURL is not defined", "", - "// Extract JSON", - "const jobs = pm.response.json().entity.jobs;", + "// Define the URL to check job statuses", + "const checkUrl = `${serverURL}/api/v1/jobs`;", "", - "// Validate is array", - "pm.expect(jobs).to.be.an('array');", + "// Function to check if there are jobs in \"FAILED\" or \"CANCELED\" state", + "function checkJobState(retriesLeft) {", "", - "// Create flags for states FAILED y CANCELED", - "let hasFailedJob = false;", - "let hasCancelledJob = false;", + " console.log(\"Checking jobs URL: \" + checkUrl);", + " const token = pm.collectionVariables.get(\"jwt\");", + " // Send a GET request to get the job statuses", + " pm.sendRequest({", + " url: checkUrl,", + " method: 'GET',", + " header: {", + " 'Authorization': `Bearer ${token}`, // Add Bearer token in Authorization header", + " 'Content-Type': 'application/json'", + " }", + " }, function (err, response) {", + " if (err) {", + " console.error(\"Error retrieving job statuses:\", err);", + " return;", + " }", "", - "// Now iterate and try to find them", - "jobs.forEach(job => {", - " if (job.state === \"FAILED\") {", - " hasFailedJob = true;", - " }", - " if (job.state === \"CANCELED\") {", - " hasCancelledJob = true;", - " }", - "});", + " let jsonData = response.json();", + " let jobs = jsonData.entity.jobs;", "", - "// Verify there is one is state FAILED", - "pm.test(\"Job with FAILED status exists\", function () {", - " pm.expect(hasFailedJob).to.be.true;", - "});", + " if (jobs.length > 0) {", + " // Check if there are jobs with \"FAILED\" and \"CANCELED\" status", + " const hasFailed = jobs.some(job => job.state === \"FAILED\");", + " const hasCanceled = jobs.some(job => job.state === \"CANCELED\");", "", - "// Verify there is one in state CANCELLED", - "pm.test(\"Job with CANCELLED status exists\", function () {", - " pm.expect(hasCancelledJob).to.be.true;", - "});" + " // Postman test to validate that there are jobs with \"FAILED\" statuses", + " pm.test(\"There are jobs in 'FAILED' state\", function () {", + " pm.expect(hasFailed).to.be.true; ", + " });", + "", + " // Postman test to validate that there are jobs with \"CANCELED\" statuses", + " pm.test(\"There are jobs in 'CANCELED' state\", function () { ", + " pm.expect(hasCanceled).to.be.true;", + " });", + "", + " if (hasFailed && hasCanceled) {", + " console.log(\"Found jobs in 'FAILED' and 'CANCELED' state.\");", + " } else {", + " console.warn(\"Did not find jobs in both 'FAILED' and 'CANCELED' states.\");", + " }", + " } else if (retriesLeft > 0) {", + " // If no jobs are found and retries are left, wait and retry", + " console.log(\"No jobs available, retries left: \" + retriesLeft);", + " setTimeout(function () {", + " checkJobState(retriesLeft - 1);", + " }, retryDelay);", + " } else {", + " // If no jobs are found and no retries are left", + " pm.test(\"Maximum retries reached, no jobs received.\", function () {", + " pm.expect(jobs.length).to.be.greaterThan(0); // This will fail if no jobs are found", + " });", + " console.warn(\"No jobs found after reaching maximum retries.\");", + " }", + " });", + "}", + "", + "// Start checking job statuses with the maximum number of retries", + "checkJobState(maxRetries);", + "" ], "type": "text/javascript", "packages": {} @@ -784,16 +1133,11 @@ } ], "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, + "type": "bearer", + "bearer": [ { - "key": "username", - "value": "admin@dotCMS.com", + "key": "token", + "value": "{{jwt}}", "type": "string" } ] @@ -805,7 +1149,45 @@ "type": "text/javascript", "packages": {}, "exec": [ - "" + " ", + "if(!pm.collectionVariables.get('jwt')){", + " console.log(\"generating....\")", + " const serverURL = pm.environment.get('serverURL') || pm.collectionVariables.get('baseUrl'); // Get the server URL from the environment variable", + " const apiUrl = `${serverURL}/api/v1/apitoken`; // Construct the full API URL", + "", + " const username = pm.environment.get(\"user\") || pm.collectionVariables.get('user'); ", + " const password = pm.environment.get(\"password\") || pm.collectionVariables.get('password');", + " const basicAuth = Buffer.from(`${username}:${password}`).toString('base64');", + "", + " const requestOptions = {", + " url: apiUrl,", + " method: \"POST\",", + " header: {", + " \"accept\": \"*/*\",", + " \"content-type\": \"application/json\",", + " \"Authorization\": `Basic ${basicAuth}`", + " },", + " body: {", + " mode: \"raw\",", + " raw: JSON.stringify({", + " \"expirationSeconds\": 7200,", + " \"userId\": \"dotcms.org.1\",", + " \"network\": \"0.0.0.0/0\",", + " \"claims\": {\"label\": \"postman-tests\"}", + " })", + " }", + " };", + "", + " pm.sendRequest(requestOptions, function (err, response) {", + " if (err) {", + " console.log(err);", + " } else {", + " const jwt = response.json().entity.jwt;", + " pm.collectionVariables.set('jwt', jwt);", + " console.log(\"Successfully got a jwt :\" + jwt);", + " }", + " }); ", + "} " ] } }, @@ -833,7 +1215,12 @@ }, { "key": "jobId", - "value": "", + "value": "-1", + "type": "string" + }, + { + "key": "cancelledJobId", + "value": "-1", "type": "string" }, { @@ -845,6 +1232,20 @@ "key": "pageSize", "value": "20", "type": "string" + }, + { + "key": "user", + "value": "admin@dotCMS.com", + "type": "string" + }, + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "jwt", + "value": "" } ] } \ No newline at end of file