From 6167b8f1b3648b457fbf769c8078b58c01bbd288 Mon Sep 17 00:00:00 2001 From: Valentino Giardino <77643678+valentinogiardino@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:42:51 -0300 Subject: [PATCH 1/2] [Content Import Job Management] Implement the Content Import Get Job Status REST endpoint #30791 (#30818) ### Proposed Changes - Enhanced `ContentImportResource` to include a new endpoint for retrieving the status of a content import job. - Introduced a new class `ResponseEntityJobView` to represent job entities in API responses. - Added validations for workflow action and key fields in `ImportContentletsProcessor`. - Introduced a method `isCsvFile` to check if the uploaded file is a CSV file, with corresponding validation added in `ContentImportParams`. ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) ### Additional Info - API updates streamline job status tracking and provide clearer data structures for frontend integration. - Refactoring the validation and workflow action handling improves error clarity and ensures robust data validation in the content import process. (This fix IQA findings on #30669) --- .../impl/ImportContentletsProcessor.java | 89 ++- .../dotcms/rest/ResponseEntityJobView.java | 9 + .../dotimport/ContentImportHelper.java | 11 + .../dotimport/ContentImportParams.java | 7 + .../dotimport/ContentImportResource.java | 57 +- ...rtContentletsProcessorIntegrationTest.java | 173 +++++- .../ContentImportResourceIntegrationTest.java | 101 +++- ...tentImportResource.postman_collection.json | 568 ++++++++++++++++-- .../resources/ContentImportResource/test.txt | 1 + 9 files changed, 914 insertions(+), 102 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/rest/ResponseEntityJobView.java create mode 100644 dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test.txt diff --git a/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletsProcessor.java b/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletsProcessor.java index 04420a9bf468..bf42bf0fc249 100644 --- a/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletsProcessor.java +++ b/dotCMS/src/main/java/com/dotcms/jobs/business/processor/impl/ImportContentletsProcessor.java @@ -23,6 +23,7 @@ import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.portlets.contentlet.action.ImportAuditUtil; import com.dotmarketing.portlets.languagesmanager.model.Language; +import com.dotmarketing.portlets.workflows.model.WorkflowAction; import com.dotmarketing.util.AdminLogger; import com.dotmarketing.util.ImportUtil; import com.dotmarketing.util.Logger; @@ -38,12 +39,7 @@ import java.io.Reader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.Calendar; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongConsumer; @@ -198,6 +194,13 @@ && getWorkflowActionId(parameters).isEmpty()) { // Make sure the content type exist (will throw an exception if it doesn't) final var contentTypeFound = findContentType(parameters); + // Make sure the workflow action exist (will throw an exception if it doesn't) + findWorkflowAction(parameters); + + // Make sure the fields exist in the content type (will throw an exception if it doesn't) + validateFields(parameters, contentTypeFound); + + // Security measure to prevent invalid attempts to import a host. final ContentType hostContentType = APILocator.getContentTypeAPI( APILocator.systemUser()).find(Host.HOST_VELOCITY_VAR_NAME); @@ -212,6 +215,30 @@ && getWorkflowActionId(parameters).isEmpty()) { } } + /** + * Validates that the fields specified in the job parameters exist in the given content type. + * + *
This method checks each field specified in the job parameters against the fields defined + * in the provided content type. If any field is not found in the content type, a + * {@link JobValidationException} is thrown.
+ * + * @param parameters The job parameters containing the fields to validate + * @param contentTypeFound The content type to validate the fields against + * @throws JobValidationException if any field specified in the parameters is not found in the content type + */ + private void validateFields(final MapThis method retrieves the workflow action ID from the given parameters and attempts to
+ * find the corresponding workflow action using the Workflow API.
+ *
+ *
+ * @param parameters a map containing parameters required for finding the workflow action,
+ * including the workflow action ID and user details.
+ *
+ * @return the {@link WorkflowAction} corresponding to the workflow action ID.
+ *
+ * @throws JobValidationException if the workflow action cannot be found.
+ * @throws JobProcessingException if an error occurs during user retrieval or
+ * workflow action lookup.
+ */
+ private WorkflowAction findWorkflowAction(final Map
+ * Expected: A JobValidationException should be thrown.
+ *
+ * @throws Exception if there's an error during the test execution
+ */
+ @Test
+ void test_process_preview_invalid_workflow_action() throws Exception {
+ ContentType testContentType = null;
+
+ try {
+ // Initialize processor
+ final var processor = new ImportContentletsProcessor();
+
+ // Create test content type
+ testContentType = createTestContentType();
+
+ // Create test CSV file
+ File csvFile = createTestCsvFile();
+
+ // Create test job
+ final var testJob = createTestJob(
+ csvFile, "preview", "en-us", testContentType.variable(),
+ "doesNotExist"
+ );
+
+ // Process the job in preview mode
+ assertThrows(JobValidationException.class, ()-> processor.validate((testJob.parameters())));
+ } finally {
+ if (testContentType != null) {
+ // Clean up test content type
+ APILocator.getContentTypeAPI(systemUser).delete(testContentType);
+ }
+ }
+ }
+
+
+ /**
+ * Scenario: Test the preview mode of the content import process with an invalid key field.
+ *
+ * Expected: A JobValidationException should be thrown.
+ *
+ * @throws Exception if there's an error during the test execution
+ */
+ @Test
+ void test_process_preview_invalid_key_field() throws Exception {
+ ContentType testContentType = null;
+
+ try {
+ // Initialize processor
+ final var processor = new ImportContentletsProcessor();
+
+ // Create test content type
+ testContentType = createTestContentType();
+
+ // Create test CSV file
+ File csvFile = createTestCsvFile();
+
+ // Create test job
+ final var testJob = createTestJob(
+ csvFile, "preview", "en-us", testContentType.variable(),
+ WORKFLOW_PUBLISH_ACTION_ID, List.of("doesNotExist")
+ );
+
+ assertThrows(JobValidationException.class, ()-> processor.validate((testJob.parameters())));
+
+ } finally {
+ if (testContentType != null) {
+ // Clean up test content type
+ APILocator.getContentTypeAPI(systemUser).delete(testContentType);
+ }
+ }
+ }
+
/**
* Tests the preview mode of the content import process. This test:
*
+ * Expected: The import request should fail with BAD_REQUEST (400) status code since the workflow action is invalid.
+ *
+ * @throws IOException if there's an error with file operations
+ * @throws DotDataException if there's an error with dotCMS data operations
+ */
+ @Test
+ public void test_import_content_with_invalid_workflow_action() throws IOException, DotDataException {
+ ContentImportForm form = createContentImportForm(contentType.name(), "12345", "workflow-action-2", null);
+ ContentImportParams params = createContentImportParams(csvFile, form);
+
+ assertBadRequestResponse(importResource.importContent(request, response, params));
+ }
+
+ /**
+ * Scenario: Attempt to import content specifying an invalid key field.
+ *
+ * Expected: The import request should fail with BAD_REQUEST (400) status code since the key field is invalid.
+ *
+ * @throws IOException if there's an error with file operations
+ * @throws DotDataException if there's an error with dotCMS data operations
+ */
+ @Test
+ public void test_import_content_with_invalid_key_field() throws IOException, DotDataException {
+ ContentImportForm form = createContentImportForm(contentType.name(), "12345", "workflow-action-2", List.of("doesNotExist"));
ContentImportParams params = createContentImportParams(csvFile, form);
assertBadRequestResponse(importResource.importContent(request, response, params));
@@ -263,7 +292,7 @@ public void test_import_content_with_invalid_content_type() throws IOException,
*/
@Test
public void test_import_content_validate_with_invalid_content_type() throws IOException, DotDataException {
- ContentImportForm form = createContentImportForm("doesNotExist", "12345", "workflow-action-id-2", null);
+ ContentImportForm form = createContentImportForm("doesNotExist", "12345", WORKFLOW_PUBLISH_ACTION_ID, null);
ContentImportParams params = createContentImportParams(csvFile, form);
assertBadRequestResponse(importResource.validateContentImport(request, response, params));
@@ -280,7 +309,7 @@ public void test_import_content_validate_with_invalid_content_type() throws IOEx
*/
@Test
public void test_import_content_without_content_type_in_form() {
- assertThrows(ValidationException.class, () -> createContentImportForm(null, null, "workflow-action-id", null));
+ assertThrows(ValidationException.class, () -> createContentImportForm(null, null, WORKFLOW_PUBLISH_ACTION_ID, null));
}
/**
@@ -307,7 +336,7 @@ public void test_import_content_without_workflow_action_in_form() {
*/
@Test
public void test_import_content_missing_file() throws JsonProcessingException {
- ContentImportForm form = createContentImportForm(contentType.name(), String.valueOf(defaultLanguage.getId()), "workflow-action-id", null);
+ ContentImportForm form = createContentImportForm(contentType.name(), String.valueOf(defaultLanguage.getId()), WORKFLOW_PUBLISH_ACTION_ID, null);
ContentImportParams params = new ContentImportParams();
params.setJsonForm(mapper.writeValueAsString(form));
@@ -327,7 +356,7 @@ public void test_import_content_missing_file() throws JsonProcessingException {
*/
@Test
public void test_import_content_validate_missing_file() throws JsonProcessingException {
- ContentImportForm form = createContentImportForm(contentType.name(), String.valueOf(defaultLanguage.getId()), "workflow-action-id", null);
+ ContentImportForm form = createContentImportForm(contentType.name(), String.valueOf(defaultLanguage.getId()), WORKFLOW_PUBLISH_ACTION_ID, null);
ContentImportParams params = new ContentImportParams();
params.setJsonForm(mapper.writeValueAsString(form));
@@ -335,6 +364,34 @@ public void test_import_content_validate_missing_file() throws JsonProcessingExc
assertThrows(ValidationException.class, () -> importResource.validateContentImport(request, response, params));
}
+
+ /**
+ * Scenario: Attempt to validate content import with valid form data but providing a txt file.
+ *
+ * Expected: A ValidationException should be thrown since the file must be a CSV file
+ * for content import operations.
+ *
+ * @throws JsonProcessingException if there's an error during JSON serialization
+ * @throws ValidationException when attempting to import content without setting the file
+ */
+ @Test
+ public void test_import_content_validate_txt_file() throws IOException, DotDataException {
+ File txtFile = null;
+ try{
+ ContentImportForm form = createContentImportForm(contentType.name(), "12345", WORKFLOW_PUBLISH_ACTION_ID, null);
+ txtFile = File.createTempFile("test", ".txt");
+ ContentImportParams params = createContentImportParams(txtFile, form);
+
+ // Assert that the response status is BAD_REQUEST (400)
+ assertBadRequestResponse(importResource.validateContentImport(request, response, params));
+ }finally {
+ if(txtFile != null && txtFile.exists()){
+ txtFile.delete();
+ }
+ }
+
+ }
+
/**
* Scenario: Attempt to import content with a valid CSV file but without providing the required form data.
*
@@ -410,7 +467,7 @@ private void validateSuccessfulResponse(Response response, String expectedConten
assertFalse(responseEntityView.getEntity().isEmpty(), "Job ID should be a non-empty string");
// Retrieve and validate job exists in the queue
- Job job = jobQueueManagerAPI.getJob(responseEntityView.getEntity());
+ Job job = contentImportHelper.getJob(responseEntityView.getEntity());
assertNotNull(job, "Job should exist in queue");
// Validate core import parameters
diff --git a/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json b/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json
index e088ca8e42a4..25cebbf18a26 100644
--- a/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json
+++ b/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json
@@ -116,41 +116,77 @@
"listen": "test",
"script": {
"exec": [
- "// Validate the response is successful",
+ "// Validate the response status is 200",
"pm.test(\"Response status is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"// Parse the response body",
- "pm.test(\"Response body should contain a valid Job ID\", function () {",
- " const responseBody = pm.response.json();",
- " pm.expect(responseBody).to.have.property('entity').that.is.not.empty;",
+ "let response = pm.response.json();",
+ "let entity = response.entity;",
+ "let parameters = entity?.parameters;",
+ "",
+ "// Validate the response body structure",
+ "pm.test(\"Response contains a valid entity\", function () {",
+ " pm.expect(response).to.have.property('entity').that.is.not.empty;",
+ " pm.expect(entity).to.be.an('object');",
"});",
"",
+ "// Validate top-level properties of the entity",
+ "pm.test(\"Entity contains expected fields\", function () {",
+ " pm.expect(entity).to.have.property('queueName', pm.collectionVariables.get('queueName'));",
+ " pm.expect(entity.queueName).to.be.a(\"string\").that.is.not.empty;",
"",
- "pm.test(\"Job entity should be defined\", function () {",
- " const responseBody = pm.response.json();",
- " const job = responseBody.entity;",
- " pm.expect(job).to.be.an('object');",
+ " pm.expect(entity).to.have.property('state').that.is.a(\"string\").that.is.not.empty;",
+ " pm.expect(entity.state).to.be.oneOf([\"RUNNING\", \"PENDING\", \"COMPLETED\"]);",
+ "",
+ " pm.expect(entity).to.have.property('progress').that.is.a(\"number\").within(0, 1);",
+ " pm.expect(entity).to.have.property('retryCount').that.is.a(\"number\").that.is.at.least(0);",
+ "",
+ " pm.expect(entity).to.have.property('result');",
+ " pm.expect(entity.result === null || typeof entity.result === \"object\").to.be.true;",
"});",
"",
+ "// Validate date fields in the entity",
+ "pm.test(\"Entity date fields are valid\", function () {",
+ " const isValidDate = (val) => val === null || new Date(val).toString() !== \"Invalid Date\";",
"",
- "pm.test(\"Job should contain correct parameters\", function () {",
- " const responseBody = pm.response.json();",
- " const job = responseBody.entity;",
- " const jobParameters = job.parameters;",
+ " pm.expect(isValidDate(entity.completedAt)).to.be.true;",
+ " pm.expect(new Date(entity.createdAt)).to.not.equal(\"Invalid Date\");",
+ " pm.expect(isValidDate(entity.startedAt)).to.be.true;",
+ " pm.expect(new Date(entity.updatedAt)).to.not.equal(\"Invalid Date\");",
+ "});",
"",
- " pm.expect(job).to.have.property('queueName', pm.collectionVariables.get('queueName'));",
- " pm.expect(jobParameters).to.be.an('object');",
- " pm.expect(jobParameters).to.have.property('cmd', 'preview');",
- " pm.expect(jobParameters).to.have.property('userId', 'dotcms.org.1');",
+ "// Validate UUID fields in the entity",
+ "pm.test(\"Entity UUID fields are valid\", function () {",
+ " const uuidRegex = /^[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.expect(jobParameters).to.have.property('contentType', pm.collectionVariables.get(\"contentType\"));",
- " pm.expect(jobParameters).to.have.property('language', pm.collectionVariables.get(\"language\"));",
- " pm.expect(jobParameters).to.have.property('workflowActionId', pm.collectionVariables.get(\"workflowActionId\"));",
- " pm.expect(jobParameters).to.have.property('fields').that.deep.equals(JSON.parse(pm.collectionVariables.get(\"fields\")));",
+ " pm.expect(entity.executionNode).to.match(uuidRegex);",
+ " pm.expect(entity.id).to.match(uuidRegex);",
+ "});",
+ "",
+ "// Validate parameters in the entity",
+ "pm.test(\"Entity parameters are valid\", function () {",
+ " pm.expect(parameters).to.be.an('object');",
+ " pm.expect(parameters).to.have.property('cmd', 'preview');",
+ " pm.expect(parameters).to.have.property('userId', 'dotcms.org.1');",
+ " pm.expect(parameters).to.have.property('contentType', pm.collectionVariables.get('contentType'));",
+ " pm.expect(parameters).to.have.property('language', pm.collectionVariables.get('language'));",
+ " pm.expect(parameters).to.have.property('workflowActionId', pm.collectionVariables.get('workflowActionId'));",
+ " pm.expect(parameters).to.have.property('fields').that.deep.equals(JSON.parse(pm.collectionVariables.get('fields')));",
+ " pm.expect(parameters).to.have.property('requestFingerPrint').that.is.a(\"string\").with.lengthOf(44);",
+ " pm.expect(parameters).to.have.property('tempFileId').that.is.a(\"string\");",
+ "});",
"",
- "});"
+ "// Validate other top-level objects in the response",
+ "pm.test(\"Response contains expected auxiliary fields\", function () {",
+ " pm.expect(response.errors).to.be.an(\"array\").that.is.empty;",
+ " pm.expect(response.i18nMessagesMap).to.be.an(\"object\").that.is.empty;",
+ " pm.expect(response.messages).to.be.an(\"array\").that.is.empty;",
+ " pm.expect(response.pagination).to.be.null;",
+ " pm.expect(response.permissions).to.be.an(\"array\").that.is.empty;",
+ "});",
+ ""
],
"type": "text/javascript",
"packages": {}
@@ -171,16 +207,16 @@
"method": "GET",
"header": [],
"url": {
- "raw": "{{serverURL}}/api/v1/jobs/{{jobId}}/status",
+ "raw": "{{serverURL}}/api/v1/content/_import/{{jobId}}",
"host": [
"{{serverURL}}"
],
"path": [
"api",
"v1",
- "jobs",
- "{{jobId}}",
- "status"
+ "content",
+ "_import",
+ "{{jobId}}"
]
}
},
@@ -509,6 +545,204 @@
},
"response": []
},
+ {
+ "name": "Validate Content Import Job With Invalid Field Expect Failure",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "// Validate the response status is 400",
+ "pm.test(\"Response status is 400\", function () {",
+ " pm.response.to.have.status(400);",
+ "});",
+ "",
+ "",
+ "// Validate that the response body contains the 'message' property and it is not empty",
+ "pm.test(\"Response should have an error message\", function () {",
+ " const responseBody = pm.response.json();",
+ " pm.expect(responseBody).to.have.property('message').that.is.not.empty;",
+ " pm.expect(responseBody.message).to.equal('Field [doesNotExist] not found in Content Type [TestImportJob].');",
+ "});",
+ ""
+ ],
+ "type": "text/javascript",
+ "packages": {}
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "resources/ContentImportResource/test-import-content-job-final.csv"
+ },
+ {
+ "key": "form",
+ "value": "{\"contentType\":\"{{contentType}}\",\"language\":\"{{language}}\",\"workflowActionId\":\"{{workflowActionId}}\", \"fields\": [\"doesNotExist\"]}",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{serverURL}}/api/v1/content/_import/_validate",
+ "host": [
+ "{{serverURL}}"
+ ],
+ "path": [
+ "api",
+ "v1",
+ "content",
+ "_import",
+ "_validate"
+ ]
+ },
+ "description": "Creates a new job in the specified queue."
+ },
+ "response": []
+ },
+ {
+ "name": "Validate Content Import Job With Invalid File Type Expect Failure",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "// Validate the response status is 400",
+ "pm.test(\"Response status is 400\", function () {",
+ " pm.response.to.have.status(400);",
+ "});",
+ "",
+ "",
+ "// Validate that the response body contains the 'message' property and it is not empty",
+ "pm.test(\"Response should have an error message\", function () {",
+ " const responseBody = pm.response.json();",
+ " pm.expect(responseBody).to.have.property('message').that.is.not.empty;",
+ " pm.expect(responseBody.message).to.equal('The file must be a CSV file.');",
+ "});",
+ ""
+ ],
+ "type": "text/javascript",
+ "packages": {}
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "resources/ContentImportResource/test.txt"
+ },
+ {
+ "key": "form",
+ "value": "{\"contentType\":\"{{contentType}}\",\"language\":\"{{language}}\",\"workflowActionId\":\"{{workflowActionId}}\", \"fields\": {{fields}}}",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{serverURL}}/api/v1/content/_import/_validate",
+ "host": [
+ "{{serverURL}}"
+ ],
+ "path": [
+ "api",
+ "v1",
+ "content",
+ "_import",
+ "_validate"
+ ]
+ },
+ "description": "Creates a new job in the specified queue."
+ },
+ "response": []
+ },
+ {
+ "name": "Validate Content Import Job With Invalid Workflow Action Expect Failure",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "// Validate the response status is 400",
+ "pm.test(\"Response status is 400\", function () {",
+ " pm.response.to.have.status(400);",
+ "});",
+ "",
+ "",
+ "// Validate that the response body contains the 'message' property and it is not empty",
+ "pm.test(\"Response should have an error message\", function () {",
+ " const responseBody = pm.response.json();",
+ " pm.expect(responseBody).to.have.property('message').that.is.not.empty;",
+ " pm.expect(responseBody.message).to.equal('Workflow Action [doesNotExtist] not found.');",
+ "});",
+ ""
+ ],
+ "type": "text/javascript",
+ "packages": {}
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "resources/ContentImportResource/test-import-content-job-final.csv"
+ },
+ {
+ "key": "form",
+ "value": "{\"contentType\":\"{{contentType}}\",\"language\":\"{{language}}\",\"workflowActionId\":\"doesNotExtist\", \"fields\": {{fields}}}",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{serverURL}}/api/v1/content/_import/_validate",
+ "host": [
+ "{{serverURL}}"
+ ],
+ "path": [
+ "api",
+ "v1",
+ "content",
+ "_import",
+ "_validate"
+ ]
+ },
+ "description": "Creates a new job in the specified queue."
+ },
+ "response": []
+ },
{
"name": "Validate Content Import Job With Invalid ContentType Expect Failure",
"event": [
@@ -760,47 +994,83 @@
"response": []
},
{
- "name": "Check Successful Job Status",
+ "name": "Check Successful Content Import Job Status",
"event": [
{
"listen": "test",
"script": {
"exec": [
- "// Validate the response is successful",
+ "// Validate the response status is 200",
"pm.test(\"Response status is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"// Parse the response body",
- "pm.test(\"Response body should contain a valid Job ID\", function () {",
- " const responseBody = pm.response.json();",
- " pm.expect(responseBody).to.have.property('entity').that.is.not.empty;",
+ "let response = pm.response.json();",
+ "let entity = response.entity;",
+ "let parameters = entity?.parameters;",
+ "",
+ "// Validate the response body structure",
+ "pm.test(\"Response contains a valid entity\", function () {",
+ " pm.expect(response).to.have.property('entity').that.is.not.empty;",
+ " pm.expect(entity).to.be.an('object');",
"});",
"",
+ "// Validate top-level properties of the entity",
+ "pm.test(\"Entity contains expected fields\", function () {",
+ " pm.expect(entity).to.have.property('queueName', pm.collectionVariables.get('queueName'));",
+ " pm.expect(entity.queueName).to.be.a(\"string\").that.is.not.empty;",
"",
- "pm.test(\"Job entity should be defined\", function () {",
- " const responseBody = pm.response.json();",
- " const job = responseBody.entity;",
- " pm.expect(job).to.be.an('object');",
+ " pm.expect(entity).to.have.property('state').that.is.a(\"string\").that.is.not.empty;",
+ " pm.expect(entity.state).to.be.oneOf([\"RUNNING\", \"PENDING\", \"COMPLETED\"]);",
+ "",
+ " pm.expect(entity).to.have.property('progress').that.is.a(\"number\").within(0, 1);",
+ " pm.expect(entity).to.have.property('retryCount').that.is.a(\"number\").that.is.at.least(0);",
+ "",
+ " pm.expect(entity).to.have.property('result');",
+ " pm.expect(entity.result === null || typeof entity.result === \"object\").to.be.true;",
"});",
"",
+ "// Validate date fields in the entity",
+ "pm.test(\"Entity date fields are valid\", function () {",
+ " const isValidDate = (val) => val === null || new Date(val).toString() !== \"Invalid Date\";",
"",
- "pm.test(\"Job should contain correct parameters\", function () {",
- " const responseBody = pm.response.json();",
- " const job = responseBody.entity;",
- " const jobParameters = job.parameters;",
+ " pm.expect(isValidDate(entity.completedAt)).to.be.true;",
+ " pm.expect(new Date(entity.createdAt)).to.not.equal(\"Invalid Date\");",
+ " pm.expect(isValidDate(entity.startedAt)).to.be.true;",
+ " pm.expect(new Date(entity.updatedAt)).to.not.equal(\"Invalid Date\");",
+ "});",
+ "",
+ "// Validate UUID fields in the entity",
+ "pm.test(\"Entity UUID fields are valid\", function () {",
+ " const uuidRegex = /^[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.expect(job).to.have.property('queueName', pm.collectionVariables.get('queueName'));",
- " pm.expect(jobParameters).to.be.an('object');",
- " pm.expect(jobParameters).to.have.property('cmd', 'publish');",
- " pm.expect(jobParameters).to.have.property('userId', 'dotcms.org.1');",
+ " pm.expect(entity.executionNode).to.match(uuidRegex);",
+ " pm.expect(entity.id).to.match(uuidRegex);",
+ "});",
"",
- " pm.expect(jobParameters).to.have.property('contentType', pm.collectionVariables.get(\"contentType\"));",
- " pm.expect(jobParameters).to.have.property('language', pm.collectionVariables.get(\"language\"));",
- " pm.expect(jobParameters).to.have.property('workflowActionId', pm.collectionVariables.get(\"workflowActionId\"));",
- " pm.expect(jobParameters).to.have.property('fields').that.deep.equals(JSON.parse(pm.collectionVariables.get(\"fields\")));",
+ "// Validate parameters in the entity",
+ "pm.test(\"Entity parameters are valid\", function () {",
+ " pm.expect(parameters).to.be.an('object');",
+ " pm.expect(parameters).to.have.property('cmd', 'publish');",
+ " pm.expect(parameters).to.have.property('userId', 'dotcms.org.1');",
+ " pm.expect(parameters).to.have.property('contentType', pm.collectionVariables.get('contentType'));",
+ " pm.expect(parameters).to.have.property('language', pm.collectionVariables.get('language'));",
+ " pm.expect(parameters).to.have.property('workflowActionId', pm.collectionVariables.get('workflowActionId'));",
+ " pm.expect(parameters).to.have.property('fields').that.deep.equals(JSON.parse(pm.collectionVariables.get('fields')));",
+ " pm.expect(parameters).to.have.property('requestFingerPrint').that.is.a(\"string\").with.lengthOf(44);",
+ " pm.expect(parameters).to.have.property('tempFileId').that.is.a(\"string\");",
+ "});",
"",
- "});"
+ "// Validate other top-level objects in the response",
+ "pm.test(\"Response contains expected auxiliary fields\", function () {",
+ " pm.expect(response.errors).to.be.an(\"array\").that.is.empty;",
+ " pm.expect(response.i18nMessagesMap).to.be.an(\"object\").that.is.empty;",
+ " pm.expect(response.messages).to.be.an(\"array\").that.is.empty;",
+ " pm.expect(response.pagination).to.be.null;",
+ " pm.expect(response.permissions).to.be.an(\"array\").that.is.empty;",
+ "});",
+ ""
],
"type": "text/javascript",
"packages": {}
@@ -821,16 +1091,16 @@
"method": "GET",
"header": [],
"url": {
- "raw": "{{serverURL}}/api/v1/jobs/{{jobId}}/status",
+ "raw": "{{serverURL}}/api/v1/content/_import/{{jobId}}",
"host": [
"{{serverURL}}"
],
"path": [
"api",
"v1",
- "jobs",
- "{{jobId}}",
- "status"
+ "content",
+ "_import",
+ "{{jobId}}"
]
}
},
@@ -1218,6 +1488,202 @@
},
"response": []
},
+ {
+ "name": "Create Import Job With Invalid Field Expect Failure Copy",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "// Validate the response status is 400",
+ "pm.test(\"Response status is 400\", function () {",
+ " pm.response.to.have.status(400);",
+ "});",
+ "",
+ "",
+ "// Validate that the response body contains the 'message' property and it is not empty",
+ "pm.test(\"Response should have an error message\", function () {",
+ " const responseBody = pm.response.json();",
+ " pm.expect(responseBody).to.have.property('message').that.is.not.empty;",
+ " pm.expect(responseBody.message).to.equal('Field [doesNotExist] not found in Content Type [TestImportJob].');",
+ "});",
+ ""
+ ],
+ "type": "text/javascript",
+ "packages": {}
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "resources/ContentImportResource/test-import-content-job-final.csv"
+ },
+ {
+ "key": "form",
+ "value": "{\"contentType\":\"{{contentType}}\",\"language\":\"{{language}}\",\"workflowActionId\":\"{{workflowActionId}}\", \"fields\": [\"doesNotExist\"]}",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{serverURL}}/api/v1/content/_import/",
+ "host": [
+ "{{serverURL}}"
+ ],
+ "path": [
+ "api",
+ "v1",
+ "content",
+ "_import",
+ ""
+ ]
+ },
+ "description": "Creates a new job in the specified queue."
+ },
+ "response": []
+ },
+ {
+ "name": "Create Import Job With Invalid File Type Expect Failure Copy",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "// Validate the response status is 400",
+ "pm.test(\"Response status is 400\", function () {",
+ " pm.response.to.have.status(400);",
+ "});",
+ "",
+ "",
+ "// Validate that the response body contains the 'message' property and it is not empty",
+ "pm.test(\"Response should have an error message\", function () {",
+ " const responseBody = pm.response.json();",
+ " pm.expect(responseBody).to.have.property('message').that.is.not.empty;",
+ " pm.expect(responseBody.message).to.equal('The file must be a CSV file.');",
+ "});",
+ ""
+ ],
+ "type": "text/javascript",
+ "packages": {}
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "resources/ContentImportResource/test.txt"
+ },
+ {
+ "key": "form",
+ "value": "{\"contentType\":\"{{contentType}}\",\"language\":\"{{language}}\",\"workflowActionId\":\"{{workflowActionId}}\", \"fields\": {{fields}}}",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{serverURL}}/api/v1/content/_import",
+ "host": [
+ "{{serverURL}}"
+ ],
+ "path": [
+ "api",
+ "v1",
+ "content",
+ "_import"
+ ]
+ },
+ "description": "Creates a new job in the specified queue."
+ },
+ "response": []
+ },
+ {
+ "name": "Create Import Job With Invalid Workflow Action Expect Failure Copy",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "// Validate the response status is 400",
+ "pm.test(\"Response status is 400\", function () {",
+ " pm.response.to.have.status(400);",
+ "});",
+ "",
+ "",
+ "// Validate that the response body contains the 'message' property and it is not empty",
+ "pm.test(\"Response should have an error message\", function () {",
+ " const responseBody = pm.response.json();",
+ " pm.expect(responseBody).to.have.property('message').that.is.not.empty;",
+ " pm.expect(responseBody.message).to.equal('Workflow Action [doesNotExtist] not found.');",
+ "});",
+ ""
+ ],
+ "type": "text/javascript",
+ "packages": {}
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "multipart/form-data"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": "resources/ContentImportResource/test-import-content-job-final.csv"
+ },
+ {
+ "key": "form",
+ "value": "{\"contentType\":\"{{contentType}}\",\"language\":\"{{language}}\",\"workflowActionId\":\"doesNotExtist\", \"fields\": {{fields}}}",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{serverURL}}/api/v1/content/_import",
+ "host": [
+ "{{serverURL}}"
+ ],
+ "path": [
+ "api",
+ "v1",
+ "content",
+ "_import"
+ ]
+ },
+ "description": "Creates a new job in the specified queue."
+ },
+ "response": []
+ },
{
"name": "Create Job Without File Expect Failure",
"event": [
diff --git a/dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test.txt b/dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test.txt
new file mode 100644
index 000000000000..30d74d258442
--- /dev/null
+++ b/dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test.txt
@@ -0,0 +1 @@
+test
\ No newline at end of file
From 83de9e0d06cfdab51021c7e25897465eb8805121 Mon Sep 17 00:00:00 2001
From: Jalinson Diaz
@@ -289,7 +361,7 @@ void test_process_preview() throws Exception {
// Create test job
final var testJob = createTestJob(
csvFile, "preview", "1", testContentType.id(),
- "b9d89c80-3d88-4311-8365-187323c96436"
+ WORKFLOW_PUBLISH_ACTION_ID
);
// Process the job in preview mode
@@ -348,7 +420,7 @@ void test_process_publish() throws Exception {
// Create test job
final var testJob = createTestJob(
csvFile, "publish", "1", testContentType.id(),
- "b9d89c80-3d88-4311-8365-187323c96436"
+ WORKFLOW_PUBLISH_ACTION_ID
);
// Process the job in preview mode
@@ -398,33 +470,91 @@ private ContentType createTestContentType() {
* @throws DotSecurityException if there's a security violation during job creation
*/
private Job createTestJob(final File csvFile, final String cmd, final String language,
- final String contentType, final String workflowActionId)
+ final String contentType, final String workflowActionId)
throws IOException, DotSecurityException {
+ return createTestJob(csvFile, cmd, language, contentType, workflowActionId, null);
+ }
- final Map