From f4195a767cf92fe38cbf04657e5badb405e74318 Mon Sep 17 00:00:00 2001 From: Valentino Giardino <77643678+valentinogiardino@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:20:04 -0300 Subject: [PATCH] feat(JobQueue) Implement import content job rest endpoint #30669 (#30698) ### Proposed Changes * **Added a new REST resource (`ContentImportResource`)** to handle content import job operations, including creating and enqueuing content import jobs. * **Added a helper class (`ContentImportHelper`)** to manage content import operations, including methods for creating and managing jobs. * **Created a bean class (`ContentImportParams`)** to encapsulate multipart form parameters for content import operations. * **Introduced a form object (`ContentImportForm`)** to represent JSON parameters for content import operations. ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) ### Additional Info These changes aim to provide a REST endpoint for managing content import jobs, enabling operations like job creation and queueing. --- .../rest/api/v1/JobQueueManagerHelper.java | 121 +++ .../v1/content/_import/ContentImportForm.java | 64 ++ .../content/_import/ContentImportHelper.java | 193 +++++ .../content/_import/ContentImportParams.java | 89 ++ .../_import/ContentImportResource.java | 131 +++ .../rest/api/v1/job/JobQueueHelper.java | 79 +- .../test/java/com/dotcms/Junit5Suite1.java | 4 +- .../src/test/java/com/dotcms/MainSuite2b.java | 2 +- .../ContentImportResourceIntegrationTest.java | 378 +++++++++ dotcms-postman/config.json | 1 + ...tentImportResource.postman_collection.json | 798 ++++++++++++++++++ .../test-import-content-job-final.csv | 2 + 12 files changed, 1796 insertions(+), 66 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/JobQueueManagerHelper.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportForm.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportHelper.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportParams.java create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportResource.java create mode 100644 dotcms-integration/src/test/java/com/dotcms/rest/api/v1/content/_import/ContentImportResourceIntegrationTest.java create mode 100644 dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json create mode 100644 dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test-import-content-job-final.csv diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/JobQueueManagerHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/JobQueueManagerHelper.java new file mode 100644 index 000000000000..38f104aed278 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/JobQueueManagerHelper.java @@ -0,0 +1,121 @@ +package com.dotcms.rest.api.v1; + +import com.dotcms.jobs.business.api.JobProcessorScanner; +import com.dotcms.jobs.business.api.JobQueueManagerAPI; +import com.dotcms.jobs.business.processor.JobProcessor; +import com.dotcms.jobs.business.processor.Queue; +import com.dotcms.util.AnnotationUtils; +import com.dotmarketing.util.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.Objects; + +/** + * Helper class for managing job queue processors in the JobQueueManagerAPI. + *

+ * This class is responsible for discovering job processors, registering them with + * the JobQueueManagerAPI, and shutting down the JobQueueManagerAPI when needed. + */ +@ApplicationScoped +public class JobQueueManagerHelper { + + private final JobQueueManagerAPI jobQueueManagerAPI; + private final JobProcessorScanner scanner; + + /** + * Constructor that injects the {@link JobProcessorScanner} and {@link JobQueueManagerAPI}. + * + * @param scanner The JobProcessorScanner to discover job processors + * @param jobQueueManagerAPI The JobQueueManagerAPI instance to register processors with + */ + @Inject + public JobQueueManagerHelper(final JobProcessorScanner scanner, final JobQueueManagerAPI jobQueueManagerAPI) { + this.scanner = scanner; + this.jobQueueManagerAPI = jobQueueManagerAPI; + } + + /** + * Default constructor required by CDI. + */ + public JobQueueManagerHelper() { + this.scanner = null; + this.jobQueueManagerAPI = null; + } + + /** + * Registers all discovered job processors with the JobQueueManagerAPI. + * If the JobQueueManagerAPI is not started, it starts the API before registering the processors. + */ + public void registerProcessors() { + if (!jobQueueManagerAPI.isStarted()) { + jobQueueManagerAPI.start(); + Logger.info(this.getClass(), "JobQueueManagerAPI started"); + } + + List> processors = scanner.discoverJobProcessors(); + processors.forEach(processor -> { + try { + if (!testInstantiation(processor)) { + return; + } + Logger.info(this.getClass(), "Registering JobProcessor: " + processor.getName()); + registerProcessor(processor); + } catch (Exception e) { + Logger.error(this.getClass(), "Unable to register JobProcessor ", e); + } + }); + } + + /** + * Tests whether a given job processor can be instantiated by attempting to + * create an instance of the processor using its default constructor. + * + * @param processor The processor class to test for instantiation + * @return true if the processor can be instantiated, false otherwise + */ + private boolean testInstantiation(final Class processor) { + try { + Constructor declaredConstructor = processor.getDeclaredConstructor(); + declaredConstructor.newInstance(); + return true; + } catch (Exception e) { + Logger.error(this.getClass(), String.format(" JobProcessor [%s] cannot be instantiated and will be ignored.", processor.getName()), e); + } + return false; + } + + /** + * Registers a job processor with the JobQueueManagerAPI using the queue name specified + * in the {@link Queue} annotation, if present. If no annotation is found, the processor's + * class name is used as the queue name. + * + * @param processor the processor class to register + */ + private void registerProcessor(final Class processor) { + Queue queue = AnnotationUtils.getBeanAnnotation(processor, Queue.class); + if (Objects.nonNull(queue)) { + jobQueueManagerAPI.registerProcessor(queue.value(), processor); + } else { + jobQueueManagerAPI.registerProcessor(processor.getName(), processor); + } + } + + /** + * Shuts down the JobQueueManagerAPI if it is currently started. + * If the JobQueueManagerAPI is started, it attempts to close it gracefully. + * In case of an error during the shutdown process, the error is logged. + */ + public void shutdown() { + if (jobQueueManagerAPI.isStarted()) { + try { + jobQueueManagerAPI.close(); + Logger.info(this.getClass(), "JobQueueManagerAPI successfully closed"); + } catch (Exception e) { + Logger.error(this.getClass(), e.getMessage(), e); + } + } + } +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportForm.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportForm.java new file mode 100644 index 000000000000..abb1cd2f446f --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportForm.java @@ -0,0 +1,64 @@ +package com.dotcms.rest.api.v1.content._import; + +import com.dotcms.repackage.javax.validation.constraints.NotNull; +import com.dotcms.rest.api.Validated; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Form object that represents the JSON parameters for content import operations. + */ +public class ContentImportForm extends Validated { + + @NotNull(message = "A Content Type id or variable is required") + private final String contentType; + + private final String language; + + @NotNull(message = "A Workflow Action id is required") + private final String workflowActionId; + + private final List fields; + + @JsonCreator + public ContentImportForm( + @JsonProperty("contentType") final String contentType, + @JsonProperty("language") final String language, + @JsonProperty("workflowActionId") final String workflowActionId, + @JsonProperty("fields") final List fields) { + super(); + this.contentType = contentType; + this.language = language; + this.workflowActionId = workflowActionId; + this.fields = fields; + this.checkValid(); + } + + public String getContentType() { + return contentType; + } + + public String getLanguage() { + return language; + } + + public String getWorkflowActionId() { + return workflowActionId; + } + + public List getFields() { + return fields; + } + + @Override + public String toString() { + return "ContentImportForm{" + + "contentType='" + contentType + '\'' + + ", language='" + language + '\'' + + ", workflowActionId='" + workflowActionId + '\'' + + ", fields=" + fields + + '}'; + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportHelper.java new file mode 100644 index 000000000000..aebe5847b44f --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportHelper.java @@ -0,0 +1,193 @@ +package com.dotcms.rest.api.v1.content._import; + +import com.dotcms.jobs.business.api.JobQueueManagerAPI; +import com.dotcms.rest.api.v1.JobQueueManagerHelper; +import com.dotcms.rest.api.v1.temp.DotTempFile; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.web.WebAPILocator; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.util.Logger; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.liferay.portal.model.User; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * Helper class for managing content import operations in the dotCMS application. + *

+ * This class provides methods to create and manage jobs for importing content + * from external sources, such as CSV files, into the system. It handles the + * validation of import parameters, processes file uploads, and constructs + * the necessary job parameters to enqueue content import tasks in the job queue. + */ +@ApplicationScoped +public class ContentImportHelper { + + private final JobQueueManagerAPI jobQueueManagerAPI; + private final JobQueueManagerHelper jobQueueManagerHelper; + + /** + * Constructor for dependency injection. + * + * @param jobQueueManagerAPI The API for managing job queues. + * @param jobQueueManagerHelper Helper for job queue management. + */ + @Inject + public ContentImportHelper(final JobQueueManagerAPI jobQueueManagerAPI, final JobQueueManagerHelper jobQueueManagerHelper) { + this.jobQueueManagerAPI = jobQueueManagerAPI; + this.jobQueueManagerHelper = jobQueueManagerHelper; + } + + /** + * Default constructor required for CDI. + */ + public ContentImportHelper() { + this.jobQueueManagerAPI = null; + this.jobQueueManagerHelper = null; + } + + /** + * Initializes the helper by registering job processors during application startup. + */ + @PostConstruct + public void onInit() { + jobQueueManagerHelper.registerProcessors(); + } + + /** + * Cleans up resources and shuts down the helper during application shutdown. + */ + @PreDestroy + public void onDestroy() { + jobQueueManagerHelper.shutdown(); + } + + /** + * Creates a content import job with the provided parameters and submits it to the job queue. + * + * @param command The command indicating the type of operation (e.g., "preview" or "import"). + * @param queueName The name of the queue to which the job should be submitted. + * @param params The content import parameters containing the details of the import operation. + * @param user The user initiating the import. + * @param request The HTTP request associated with the import operation. + * @return The ID of the created job. + * @throws DotDataException If there is an error creating the job. + * @throws JsonProcessingException If there is an error processing JSON data. + */ + public String createJob( + final String command, + final String queueName, + final ContentImportParams params, + final User user, + final HttpServletRequest request) throws DotDataException, JsonProcessingException { + + params.checkValid(); + final Map jobParameters = createJobParameters(command, params, user, request); + processFileUpload(params, jobParameters, request); + + return jobQueueManagerAPI.createJob(queueName, jobParameters); + } + + /** + * Constructs a map of job parameters based on the provided inputs. + * + * @param command The command indicating the type of operation. + * @param params The content import parameters. + * @param user The user initiating the import. + * @param request The HTTP request associated with the operation. + * @return A map containing the job parameters. + * @throws JsonProcessingException If there is an error processing JSON data. + */ + private Map createJobParameters( + final String command, + final ContentImportParams params, + final User user, + final HttpServletRequest request) throws JsonProcessingException { + + final Map jobParameters = new HashMap<>(); + + // Add required parameters + jobParameters.put("cmd", command); + jobParameters.put("userId", user.getUserId()); + jobParameters.put("contentType", params.getForm().getContentType()); + jobParameters.put("workflowActionId", params.getForm().getWorkflowActionId()); + + // Add optional parameters + addOptionalParameters(params, jobParameters); + + // Add site information + addSiteInformation(request, jobParameters); + + return jobParameters; + } + + /** + * Adds optional parameters to the job parameter map if they are present in the form. + * + * @param params The content import parameters. + * @param jobParameters The map of job parameters to which optional parameters are added. + * @throws JsonProcessingException If there is an error processing JSON data. + */ + private void addOptionalParameters( + final ContentImportParams params, + final Map jobParameters) throws JsonProcessingException { + + final ContentImportForm form = params.getForm(); + + if (form.getLanguage() != null && !form.getLanguage().isEmpty()) { + jobParameters.put("language", form.getLanguage()); + } + if (form.getFields() != null && !form.getFields().isEmpty()) { + jobParameters.put("fields", form.getFields()); + } + } + + /** + * Adds the current site information to the job parameters. + * + * @param request The HTTP request associated with the operation. + * @param jobParameters The map of job parameters to which site information is added. + */ + private void addSiteInformation( + final HttpServletRequest request, + final Map jobParameters){ + + final var currentHost = WebAPILocator.getHostWebAPI().getCurrentHostNoThrow(request); + jobParameters.put("siteName", currentHost.getHostname()); + jobParameters.put("siteIdentifier", currentHost.getIdentifier()); + } + + /** + * Processes the file upload and adds the file-related parameters to the job. + * + * @param params The content import parameters. + * @param jobParameters The map of job parameters. + * @param request The HTTP request containing the uploaded file. + * @throws DotDataException If there is an error processing the file upload. + */ + private void processFileUpload( + final ContentImportParams params, + final Map jobParameters, + final HttpServletRequest request) throws DotDataException { + + try { + final DotTempFile tempFile = APILocator.getTempFileAPI().createTempFile( + params.getContentDisposition().getFileName(), + request, + params.getFileInputStream() + ); + jobParameters.put("tempFileId", tempFile.id); + jobParameters.put("requestFingerPrint", APILocator.getTempFileAPI().getRequestFingerprint(request)); + } catch (DotSecurityException e) { + Logger.error(this, "Error handling file upload", e); + throw new DotDataException("Error processing file upload: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportParams.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportParams.java new file mode 100644 index 000000000000..61e993280d11 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportParams.java @@ -0,0 +1,89 @@ +package com.dotcms.rest.api.v1.content._import; + +import com.dotcms.repackage.javax.validation.ValidationException; +import com.dotcms.repackage.javax.validation.constraints.NotNull; +import com.dotcms.rest.api.Validated; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.minidev.json.annotate.JsonIgnore; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import java.io.InputStream; + +/** + * Bean class that encapsulates the multipart form parameters for content import operations. + */ +public class ContentImportParams extends Validated { + + @NotNull(message = "The file is required.") + @FormDataParam("file") + private InputStream fileInputStream; + + @JsonIgnore + @FormDataParam("file") + private FormDataContentDisposition contentDisposition; + + @FormDataParam("form") + private ContentImportForm form; + + @NotNull(message = "The form data is required.") + @FormDataParam("form") + private String jsonForm; + + public InputStream getFileInputStream() { + return fileInputStream; + } + + public void setFileInputStream(InputStream fileInputStream) { + this.fileInputStream = fileInputStream; + } + + public FormDataContentDisposition getContentDisposition() { + return contentDisposition; + } + + public void setContentDisposition(FormDataContentDisposition contentDisposition) { + this.contentDisposition = contentDisposition; + } + + public void setJsonForm(String jsonForm) { + this.jsonForm = jsonForm; + } + + public String getJsonForm() { + return jsonForm; + } + + public void setForm(ContentImportForm form) { + this.form = form; + } + + /** + * Gets the parsed form object, lazily parsing the JSON if needed + * @return The ContentImportForm object + */ + public ContentImportForm getForm() throws JsonProcessingException { + if (null == form && (null != jsonForm && !jsonForm.isEmpty())) { + form = new ObjectMapper().readValue(jsonForm, ContentImportForm.class); + } + return form; + } + + @Override + public String toString() { + return "ContentImportParams{" + + "form=" + getJsonForm() + + ", hasFile=" + (fileInputStream != null) + + ", fileName=" + (contentDisposition != null ? contentDisposition.getFileName() : "null") + + '}'; + } + + @Override + public void checkValid() { + super.checkValid(); + if (contentDisposition == null || contentDisposition.getFileName() == null) { + throw new ValidationException("The file must have a valid file name."); + } + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportResource.java new file mode 100644 index 000000000000..07319756379d --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/content/_import/ContentImportResource.java @@ -0,0 +1,131 @@ +package com.dotcms.rest.api.v1.content._import; + +import com.dotcms.jobs.business.error.JobValidationException; +import com.dotcms.rest.ResponseEntityView; +import com.dotcms.rest.WebResource; +import com.dotcms.rest.exception.mapper.ExceptionMapperUtil; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.util.Logger; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + + +/** + * REST resource for handling content import operations, including creating and enqueuing content import jobs. + * This class provides endpoints for importing content from CSV files and processing them based on the provided parameters. + */ +@Path("/v1/content") +public class ContentImportResource { + + private final WebResource webResource; + private final ContentImportHelper importHelper; + private static final String IMPORT_QUEUE_NAME = "importContentlets"; + + // Constants for commands + private static final String CMD_PUBLISH = com.dotmarketing.util.Constants.PUBLISH; + + /** + * Constructor for ContentImportResource. + * + * @param importHelper The helper class used to manage content import jobs + */ + @Inject + public ContentImportResource(final ContentImportHelper importHelper) { + this(new WebResource(), importHelper); + } + + /** + * Constructor for ContentImportResource with WebResource and ContentImportHelper injected. + * + * @param webResource The web resource for handling HTTP requests and responses + * @param importHelper The helper class used to manage content import jobs + */ + public ContentImportResource(final WebResource webResource, final ContentImportHelper importHelper) { + this.webResource = webResource; + this.importHelper = importHelper; + } + + /** + * Creates and enqueues a new content import job, processing a CSV file with specified parameters. + * + * @param request The HTTP servlet request containing user and context information + * @param response The HTTP servlet response that will contain the response to the client + * @param params The import parameters, including: + * - file: The CSV file to import + * - contentType: The content type variable or ID (required) + * - language: The language code (e.g., "en-US") or ID + * - workflowActionId: The workflow action ID to apply (required) + * - fields: List of fields to use as keys for updates + * + * @return A Response containing the job ID if the import job was successfully created, or an error response if validation fails + * @throws DotDataException If there is an issue with DotData during the import process + * @throws JsonProcessingException If there is an issue processing the JSON response + */ + @POST + @Path("/_import") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + operationId = "importContent", + summary = "Imports content from a CSV file", + description = "Creates and enqueues a new content import job based on the provided parameters. The job processes a CSV file and updates content based on the specified content type, language, and workflow action.", + tags = {"Content Import"}, + responses = { + @ApiResponse( + responseCode = "200", + description = "Content import job created successfully", + content = @Content(mediaType = "application/json", + examples = @ExampleObject(value = "{\n" + + " \"entity\": \"3930f815-7aa4-4649-94c2-3f37fd21136d\",\n" + + " \"errors\": [],\n" + + " \"i18nMessagesMap\": {},\n" + + " \"messages\": [],\n" + + " \"pagination\": null,\n" + + " \"permissions\": []\n" + + "}") + ) + ), + @ApiResponse(responseCode = "400", description = "Bad request due to validation errors"), + @ApiResponse(responseCode = "401", description = "Invalid user authentication"), + @ApiResponse(responseCode = "403", description = "Forbidden due to insufficient permissions"), + @ApiResponse(responseCode = "404", description = "Content type or language not found"), + @ApiResponse(responseCode = "500", description = "Internal server error") + } + ) + public Response importContent( + @Context final HttpServletRequest request, + @Context final HttpServletResponse response, + @BeanParam final ContentImportParams params) + throws DotDataException, JsonProcessingException { + + // Initialize the WebResource and set required user information + final var initDataObject = new WebResource.InitBuilder(webResource) + .requiredBackendUser(true) + .requiredFrontendUser(false) + .requestAndResponse(request, response) + .rejectWhenNoUser(true) + .init(); + + Logger.debug(this, ()->String.format(" user %s is importing content: %s", initDataObject.getUser().getUserId(), params)); + + try { + // Create the import job + final String jobId = importHelper.createJob(CMD_PUBLISH, IMPORT_QUEUE_NAME, params, initDataObject.getUser(), request); + return Response.ok(new ResponseEntityView<>(jobId)).build(); + } catch (JobValidationException e) { + // Handle validation exception and return appropriate error message + return ExceptionMapperUtil.createResponse(null, e.getMessage()); + } + } +} \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueHelper.java index 534986fb7b37..d57d5d3edfaf 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueHelper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/job/JobQueueHelper.java @@ -2,14 +2,13 @@ import static com.dotcms.jobs.business.util.JobUtil.roundedProgress; -import com.dotcms.jobs.business.api.JobProcessorScanner; import com.dotcms.jobs.business.api.JobQueueManagerAPI; import com.dotcms.jobs.business.error.JobProcessorNotFoundException; import com.dotcms.jobs.business.job.Job; import com.dotcms.jobs.business.job.JobPaginatedResult; import com.dotcms.jobs.business.job.JobState; import com.dotcms.jobs.business.processor.JobProcessor; -import com.dotcms.jobs.business.processor.Queue; +import com.dotcms.rest.api.v1.JobQueueManagerHelper; import com.dotcms.rest.api.v1.temp.DotTempFile; import com.dotcms.rest.api.v1.temp.TempFileAPI; import com.dotmarketing.business.APILocator; @@ -22,10 +21,8 @@ import com.liferay.portal.model.User; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Constructor; import java.time.format.DateTimeFormatter; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -45,73 +42,17 @@ @ApplicationScoped public class JobQueueHelper { - JobQueueManagerAPI jobQueueManagerAPI; - - JobProcessorScanner scanner; + private JobQueueManagerAPI jobQueueManagerAPI; + private JobQueueManagerHelper jobQueueManagerHelper; public JobQueueHelper() { //default constructor Mandatory for CDI } - @PostConstruct - public void onInit() { - - if(!jobQueueManagerAPI.isStarted()){ - jobQueueManagerAPI.start(); - Logger.info(this.getClass(), "JobQueueManagerAPI started"); - } - final List> processors = scanner.discoverJobProcessors(); - processors.forEach(processor -> { - try { - if(!testInstantiation(processor)){ - return; - } - //registering the processor with the jobQueueManagerAPI - // lower case it to avoid case - if(processor.isAnnotationPresent(Queue.class)){ - final Queue queue = processor.getAnnotation(Queue.class); - jobQueueManagerAPI.registerProcessor(queue.value(), processor); - } else { - jobQueueManagerAPI.registerProcessor(processor.getName(), processor); - } - }catch (Exception e){ - Logger.error(this.getClass(), "Unable to register JobProcessor ", e); - } - }); - } - - /** - * Test if a processor can be instantiated - * @param processor The processor to tested - * @return true if the processor can be instantiated, false otherwise - */ - private boolean testInstantiation(Class processor) { - try { - final Constructor declaredConstructor = processor.getDeclaredConstructor(); - declaredConstructor.newInstance(); - return true; - } catch (Exception e) { - Logger.error(this.getClass(), String.format(" JobProcessor [%s] can not be instantiated and will be ignored.",processor.getName()), e); - } - return false; - } - - @PreDestroy - public void onDestroy() { - if(jobQueueManagerAPI.isStarted()){ - try { - jobQueueManagerAPI.close(); - Logger.info(this.getClass(), "JobQueueManagerAPI successfully closed"); - } catch (Exception e) { - Logger.error(this.getClass(), e.getMessage(), e); - } - } - } - @Inject - public JobQueueHelper(JobQueueManagerAPI jobQueueManagerAPI, JobProcessorScanner scanner) { + public JobQueueHelper(JobQueueManagerAPI jobQueueManagerAPI, JobQueueManagerHelper jobQueueManagerHelper) { this.jobQueueManagerAPI = jobQueueManagerAPI; - this.scanner = scanner; + this.jobQueueManagerHelper = jobQueueManagerHelper; } /** @@ -124,6 +65,16 @@ void registerProcessor(final String queueName, final Class + * Expected: A new import job should be created successfully with all parameters properly set. + * + * @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_valid_params() throws IOException, DotDataException { + ContentImportForm form = createContentImportForm(contentType.name(), String.valueOf(defaultLanguage.getId()), "workflow-action-id", List.of("title")); + ContentImportParams params = createContentImportParams(csvFile, form); + + Response importContentResponse = importResource.importContent(request, response, params); + validateSuccessfulResponse(importContentResponse, contentType.name(), String.valueOf(defaultLanguage.getId()), List.of("title"), "workflow-action-id", CMD_PUBLISH); + } + + /** + * Scenario: Import content with all parameters using the language ISO code + *

+ * Expected: A new import job should be created successfully with all parameters properly set. + * + * @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_language_iso_code() throws IOException, DotDataException { + ContentImportForm form = createContentImportForm(contentType.name(), defaultLanguage.getIsoCode(), "workflow-action-id", List.of("title")); + ContentImportParams params = createContentImportParams(csvFile, form); + + Response importContentResponse = importResource.importContent(request, response, params); + validateSuccessfulResponse(importContentResponse, contentType.name(), defaultLanguage.getIsoCode(), List.of("title"), "workflow-action-id", CMD_PUBLISH); + } + + /** + * Scenario: Attempt to import content without specifying language and fields parameters. + *

+ * Expected: The import request should fail with BAD_REQUEST (400) status code. + * A key identifying the different Language versions of the same content must be defined + * when importing multilingual files + * + * @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_without_language_and_field_params() throws IOException, DotDataException { + ContentImportForm form = createContentImportForm(contentType.name(), null, "workflow-action-id-2", null); + ContentImportParams params = createContentImportParams(csvFile, form); + + // Assert that the response status is BAD_REQUEST (400) + assertBadRequestResponse(importResource.importContent(request, response, params)); + } + + /** + * Scenario: Attempt to import content specifying a non-existing language. + *

+ * Expected: The import request should fail with BAD_REQUEST (400) status code. + * + * @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_language() throws IOException, DotDataException { + ContentImportForm form = createContentImportForm(contentType.name(), "12345", "workflow-action-id-2", null); + ContentImportParams params = createContentImportParams(csvFile, form); + + assertBadRequestResponse(importResource.importContent(request, response, params)); + } + + /** + * Scenario: Attempt to import content specifying a non-existing content-type. + *

+ * Expected: The import request should fail with BAD_REQUEST (400) status code since the content type 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_content_type() throws IOException, DotDataException { + ContentImportForm form = createContentImportForm("doesNotExist", "12345", "workflow-action-id-2", null); + ContentImportParams params = createContentImportParams(csvFile, form); + + assertBadRequestResponse(importResource.importContent(request, response, params)); + } + + /** + * Scenario: Attempt to create an import form without specifying the required content type parameter. + *

+ * Expected: A ValidationException should be thrown since content type is a required parameter + * for content import operations. + * A Content Type id or variable is required. + * + * @throws ValidationException when attempting to create a form without content type + */ + @Test + public void test_import_content_without_content_type_in_form() { + assertThrows(ValidationException.class, () -> createContentImportForm(null, null, "workflow-action-id", null)); + } + + /** + * Scenario: Attempt to create an import form without specifying the required workflow action parameter. + *

+ * Expected: A ValidationException should be thrown since workflow action is a required parameter + * for content import operations. + * + * @throws ValidationException when attempting to create a form without workflow action + */ + @Test + public void test_import_content_without_workflow_action_in_form() { + assertThrows(ValidationException.class, () -> createContentImportForm(contentType.name(), null, null, null)); + } + + /** + * Scenario: Attempt to import content with valid form data but without providing the required CSV file. + *

+ * Expected: A ValidationException should be thrown since the file is a required parameter + * 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_missing_file() throws JsonProcessingException { + ContentImportForm form = createContentImportForm(contentType.name(), String.valueOf(defaultLanguage.getId()), "workflow-action-id", null); + + ContentImportParams params = new ContentImportParams(); + params.setJsonForm(mapper.writeValueAsString(form)); + + assertThrows(ValidationException.class, () -> importResource.importContent(request, response, params)); + } + + /** + * Scenario: Attempt to import content with a valid CSV file but without providing the required form data. + *

+ * Expected: A ValidationException should be thrown since form data is a required parameter + * for content import operations. + * + * @throws IOException if there's an error during file operations + * @throws ValidationException when attempting to import content without setting form data + */ + @Test + public void test_import_content_missing_form() throws IOException { + ContentImportParams params = new ContentImportParams(); + params.setFileInputStream(new FileInputStream(csvFile)); + params.setContentDisposition(createContentDisposition(csvFile.getName())); + + assertThrows(ValidationException.class, () -> importResource.importContent(request, response, params)); + } + + /** + * Validates the response and job parameters from a content import operation. + *

+ * Performs the following validations: + * - Response status is OK (200) + * - Response entity is properly formatted + * - Job exists in the queue + * - All job parameters match expected values + * - Optional fields are properly set when provided + * + * @param response The Response object from the import operation + * @param expectedContentType The content type that should be set in the job + * @param expectedLanguage The language ID that should be set in the job + * @param expectedFields List of fields that should be included in the job, or null if no fields expected + * @param expectedWorkflowActionId The workflow action ID that should be set in the job + * @param expectedCommand The command that should be set in the job (usually 'publish') + * @throws DotDataException if there's an error retrieving the job from the queue + * @throws AssertionError if any validation fails + */ + private void validateSuccessfulResponse(Response response, String expectedContentType, String expectedLanguage, List expectedFields, String expectedWorkflowActionId, String expectedCommand) throws DotDataException { + // Validate Response object + assertNotNull(response, "Import response should not be null"); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus(), "Response status should be OK"); + + // Check and cast the entity safely + Object entity = response.getEntity(); + assertNotNull(entity, "Response entity should not be null"); + assertInstanceOf(ResponseEntityView.class, entity, "Entity should be of type ResponseEntityView"); + + @SuppressWarnings("unchecked") + ResponseEntityView responseEntityView = (ResponseEntityView) entity; + + // Validate response object and job ID existence + assertNotNull(responseEntityView, "ResponseEntityView should not be null"); + assertNotNull(responseEntityView.getEntity(), "Job ID should not be null"); + 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()); + assertNotNull(job, "Job should exist in queue"); + + // Validate core import parameters + assertEquals(expectedContentType, job.parameters().get("contentType"), "Job should contain correct content type"); + assertEquals(expectedLanguage, job.parameters().get("language"), "Job should contain correct language"); + assertEquals(expectedWorkflowActionId, job.parameters().get("workflowActionId"), "Job should contain correct workflow action"); + + // Validate job configuration and metadata + assertEquals(IMPORT_QUEUE_NAME, job.queueName(), "Job should be in the correct queue"); + assertEquals(expectedCommand, job.parameters().get("cmd").toString(), "Job command should be 'publish'"); + assertEquals(defaultSite.getIdentifier(), job.parameters().get("siteIdentifier"), "Job should contain correct site identifier"); + assertEquals(adminUser.getUserId(), job.parameters().get("userId"), "Job should contain correct user ID"); + + // Validate optional fields parameter + if (expectedFields != null) { + assertTrue(job.parameters().containsKey("fields"), "Job should contain fields"); + assertEquals(expectedFields, job.parameters().get("fields"), "Job should contain correct fields"); + } else { + assertFalse(job.parameters().containsKey("fields"), "Job should not contain fields"); + } + } + + /** + * Creates a temporary CSV file for testing purposes. + * The file contains two rows of test data with 'title' and 'body' columns. + * + * @return A temporary File object containing test CSV data + * @throws IOException if there's an error creating or writing to the temporary file + */ + private static File createTestCsvFile() throws IOException { + String csv = "title,body\nTest Title 1,Test Body 1\nTest Title 2,Test Body 2\n"; + File csvFile = File.createTempFile("test", ".csv"); + Files.write(csvFile.toPath(), csv.getBytes()); + return csvFile; + } + + /** + * Creates a FormDataContentDisposition object for file upload testing. + * Sets up the basic metadata required for a file upload including name and size. + * + * @param filename The name of the file to be included in the content disposition + * @return A FormDataContentDisposition object configured for testing + */ + private FormDataContentDisposition createContentDisposition(String filename) { + return FormDataContentDisposition + .name("file") + .fileName(filename) + .size(100L) + .build(); + } + + /** + * Creates a ContentImportParams object with all required parameters for content import. + * Includes file input stream, content disposition, and JSON form data. + * + * @param file The CSV file to be imported + * @param form The form containing import configuration parameters + * @return A fully configured ContentImportParams object + * @throws IOException if there's an error reading the file or serializing the form to JSON + */ + private ContentImportParams createContentImportParams(File file, ContentImportForm form) throws IOException { + ContentImportParams params = new ContentImportParams(); + params.setFileInputStream(new FileInputStream(file)); + params.setContentDisposition(createContentDisposition(file.getName())); + params.setJsonForm(mapper.writeValueAsString(form)); + return params; + } + + /** + * Creates a ContentImportForm with the specified parameters for content import configuration. + * + * @param contentType The type of content to be imported + * @param language The language ID for the imported content + * @param workflowActionId The ID of the workflow action to be applied + * @param fields List of fields to be included in the import + * @return A ContentImportForm configured with the specified parameters + * @throws ValidationException if required parameters (contentType or workflowActionId) are missing + */ + private ContentImportForm createContentImportForm(String contentType, String language, + String workflowActionId, List fields) { + return new ContentImportForm(contentType, language, workflowActionId, fields); + } + + /** + * Asserts that the given response has a status of BAD_REQUEST (400). + * + *

This method checks that the HTTP response status code is 400 (BAD_REQUEST). + * It is commonly used in test cases where the expected response is an error due to invalid input or request.

+ * + * @param importContentResponse the HTTP response to check + * @throws AssertionError if the response status is not BAD_REQUEST + */ + private void assertBadRequestResponse(Response importContentResponse) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), importContentResponse.getStatus(), "Expected BAD_REQUEST status"); + } +} diff --git a/dotcms-postman/config.json b/dotcms-postman/config.json index 1490c0b9900e..06abde738b82 100644 --- a/dotcms-postman/config.json +++ b/dotcms-postman/config.json @@ -52,6 +52,7 @@ "name": "default-split", "collections": [ "ApiToken_Resource.postman_collection", + "ContentImportResource.postman_collection", "Manifest_Download_End_Point.postman_collection", "Osgi.postman_collection", "Page_Version_with_different_Templates.postman_collection", diff --git a/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json b/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json new file mode 100644 index 000000000000..84d80490cada --- /dev/null +++ b/dotcms-postman/src/main/resources/postman/ContentImportResource.postman_collection.json @@ -0,0 +1,798 @@ +{ + "info": { + "_postman_id": "a089a839-9ea3-4498-88a1-f94e76e04b3c", + "name": "ContentImportResource", + "description": "Postman collection for testing the ContentImportResource API endpoints.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "36604690" + }, + "item": [ + { + "name": "pre-execution-scripts", + "item": [ + { + "name": "Create ContentType Copy", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"clazz\":\"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"defaultType\":false,\n \"name\":\"TestImportJob\",\n \"description\":\"THE DESCRIPTION\",\n \"host\":\"SYSTEM_HOST\",\n \"owner\":\"dotcms.org.1\",\n \"fixed\":false,\n \"system\":false,\n \"folder\":\"SYSTEM_FOLDER\",\n \"workflow\": [\"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"],\n \"fields\":[\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableHostFolderField\",\n \"dataType\":\"SYSTEM\",\n \"fieldType\":\"Host-Folder\",\n \"fieldTypeLabel\":\"Site or Folder\",\n \"fixed\":false,\n \"forceIncludeInApi\":false,\n \"indexed\":true,\n \"listed\":false,\n \"name\":\"Host\",\n \"readOnly\":false,\n \"required\":true,\n \"searchable\":false,\n \"sortOrder\":2,\n \"unique\":false,\n \"variable\":\"contentHost\"\n },\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextField\",\n \"dataType\":\"TEXT\",\n \"fieldType\":\"Text\",\n \"fieldTypeLabel\":\"Text\",\n \"fixed\":false,\n \"forceIncludeInApi\":false,\n \"indexed\":true,\n \"listed\":true,\n \"name\":\"Title\",\n \"readOnly\":false,\n \"required\":true,\n \"searchable\":true,\n \"sortOrder\":3,\n \"unique\":false,\n \"variable\":\"title\"\n },\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextAreaField\",\n \"dataType\":\"LONG_TEXT\",\n \"fieldType\":\"Textarea\",\n \"fieldTypeLabel\":\"Textarea\",\n \"fixed\":false,\n \"forceIncludeInApi\":false,\n \"indexed\":false,\n \"listed\":false,\n \"name\":\"Description\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":5,\n \"unique\":false,\n \"variable\":\"description\"\n },\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTagField\",\n \"dataType\":\"SYSTEM\",\n \"fieldType\":\"Tag\",\n \"fieldTypeLabel\":\"Tag\",\n \"fixed\":false,\n \"forceIncludeInApi\":false,\n \"indexed\":true,\n \"listed\":false,\n \"name\":\"Tags\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":6,\n \"unique\":false,\n \"variable\":\"tags\"\n },\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableImageField\",\n \"dataType\":\"TEXT\",\n \"fieldType\":\"Image\",\n \"fieldTypeLabel\":\"Image\",\n \"fieldVariables\":[\n \n ],\n \"fixed\":false,\n \"forceIncludeInApi\":false,\n \"indexed\":false,\n \"listed\":false,\n \"name\":\"Image\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":8,\n \"unique\":false,\n \"variable\":\"image\"\n },\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextField\",\n \"dataType\":\"TEXT\",\n \"fieldType\":\"Text\",\n \"fieldTypeLabel\":\"Text\",\n \"fieldVariables\":[\n \n ],\n \"fixed\":false,\n \"forceIncludeInApi\":false,\n \"indexed\":false,\n \"listed\":false,\n \"name\":\"Alt Tag\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":9,\n \"unique\":false,\n \"variable\":\"altTag\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/contenttype", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype" + ] + }, + "description": "Given a content type payload containing field variables.\nWhen sending a POST.\nExpect that code is 200.\nExpect content type is created with the provided fields.\nExpect that new properties of content types are set (icon and sortOrder)." + }, + "response": [] + } + ] + }, + { + "name": "Create Valid Import Job Expect Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate the response is successful", + "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;", + " pm.collectionVariables.set(\"jobId\", responseBody.entity);", + "});", + "" + ], + "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\": {{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": "Check Successful Job Status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate the response is successful", + "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;", + "});", + "", + "", + "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.test(\"Job should contain correct parameters\", function () {", + " const responseBody = pm.response.json();", + " const job = responseBody.entity;", + " const jobParameters = job.parameters;", + "", + " 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(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\")));", + "", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/jobs/{{jobId}}/status", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "jobs", + "{{jobId}}", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Create Import Job Without Fields Expect Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate the response is successful", + "pm.test(\"Response status is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "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;", + "});", + "" + ], + "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}}\"}", + "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 ISO Languag Expect Success Copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate the response is successful", + "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;", + "});", + "" + ], + "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\":\"en-us\",\"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 Without Language Expect Success", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate the response is successful", + "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;", + "});", + "" + ], + "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}}\",\"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 Without Language and 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('A key identifying the different Language versions of the same content must be defined when importing multilingual files.');", + "});", + "" + ], + "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}}\",\"workflowActionId\":\"{{workflowActionId}}\"}", + "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 language 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('Language [54321] 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\":\"54321\",\"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 ContentType 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('Content Type [doesNotExist] 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\":\"doesNotExist\",\"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 Job Without File Expect Failure", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Validate missing file throws the correct error\", function () {", + " pm.response.to.have.status(400);", + " const errors = pm.response.json();", + "", + " const fileError = errors.find(error => error.fieldName === \"fileInputStream\");", + " pm.expect(fileError).to.be.an(\"object\");", + " pm.expect(fileError).to.have.property(\"message\", \"The file is required.\");", + " pm.expect(fileError).to.have.property(\"errorCode\", null);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "multipart/form-data" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "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 Job Without Form Expect Failure", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Validate missing form data throws the correct error\", function () {", + " pm.response.to.have.status(400);", + " const errors = pm.response.json();", + "", + " const formError = errors.find(error => error.fieldName === \"jsonForm\");", + " pm.expect(formError).to.be.an(\"object\");", + " pm.expect(formError).to.have.property(\"message\", \"The form data is required.\");", + " pm.expect(formError).to.have.property(\"errorCode\", null);", + "});", + "" + ], + "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" + } + ] + }, + "url": { + "raw": "{{serverURL}}/api/v1/content/_import", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "content", + "_import" + ] + }, + "description": "Creates a new job in the specified queue." + }, + "response": [] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "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);", + " }", + " }); ", + "} " + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "queueName", + "value": "importContentlets", + "type": "string" + }, + { + "key": "jobId", + "value": "-1", + "type": "string" + }, + { + "key": "user", + "value": "admin@dotCMS.com", + "type": "string" + }, + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "jwt", + "value": "" + }, + { + "key": "contentType", + "value": "TestImportJob", + "type": "string" + }, + { + "key": "language", + "value": "1", + "type": "string" + }, + { + "key": "workflowActionId", + "value": "b9d89c80-3d88-4311-8365-187323c96436", + "type": "string" + }, + { + "key": "fields", + "value": "[\"title\"]", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test-import-content-job-final.csv b/dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test-import-content-job-final.csv new file mode 100644 index 000000000000..317701b37337 --- /dev/null +++ b/dotcms-postman/src/main/resources/postman/resources/ContentImportResource/test-import-content-job-final.csv @@ -0,0 +1,2 @@ +contentHost,title,description,tags,image,altTag +48190c8c-42c4-46af-8d1a-0cd5db894797,Import Job Test Final,test desc,testTag,, \ No newline at end of file