diff --git a/README.md b/README.md index 31babe6da..3aee2b894 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Table of Contents * [Actions](#actions) * [Data Persistence and Defining Inputs](#data-persistence-and-defining-inputs) * [Submission Object](#submission-object) + * [Short Code](#submission-short-code) * [Inputs Class](#inputs-class) * [Validating Inputs](#validating-inputs) * [Required Inputs](#required-inputs) @@ -540,6 +541,40 @@ progresses. This field is placed in the model handed to the Thymeleaf templates, should have access to it. +### Submission Short Code + +A genericized implementation that can be used, among other things, as a unique confirmation code after +completion of the flow. An example of a 6 character, all uppercase, alphanumeric code is 8H7LP2. + +The short code is accessible via `getShortCode()`. It is created by default in the `ScreenController` +after the Submission has been submitted. This can be changed via `ShortCodeConfig`'s `creationPoint` +to be generated and set after the initial creation of the Submission. It is configurable for length, +forced uppercase, character set, and creation point. + +```yaml +form-flow: + short-code: + # default = 8 + length: 8 + # default = alphanumeric | options: alphanumeric (A-z 0-9), alpha (A-z), numeric (0-9) + type: alphanumeric + # default = true | options: true, false + uppercase: false + # default = submission | options: submission, creation + creation-point: submission + # default = null + prefix: IL- + # default = null + suffix: -APP +``` + +On creation of the short code, uniqueness is guaranteed. Because of that, it is incredibly important to +be sure the configuration allows for enough possible permutations in your data set. A minimum of 6 +characters is recommended. + +The `SubmissionRepositoryService` allows for reverse lookup of the Submission by the Short Code using +`findByShortCode`. + ## Inputs Class The inputs class's location is defined by the application using this library. Applications will need diff --git a/src/main/java/formflow/library/ScreenController.java b/src/main/java/formflow/library/ScreenController.java index 7dd8e38e1..571a8b36b 100644 --- a/src/main/java/formflow/library/ScreenController.java +++ b/src/main/java/formflow/library/ScreenController.java @@ -12,6 +12,7 @@ import formflow.library.config.NextScreen; import formflow.library.config.ScreenNavigationConfiguration; import formflow.library.config.SubflowConfiguration; +import formflow.library.config.submission.ShortCodeConfig; import formflow.library.data.FormSubmission; import formflow.library.data.Submission; import formflow.library.data.SubmissionRepositoryService; @@ -68,6 +69,8 @@ public class ScreenController extends FormFlowController { private final ConditionManager conditionManager; private final ActionManager actionManager; private final FileValidationService fileValidationService; + private final SubmissionRepositoryService submissionRepositoryService; + private final ShortCodeConfig shortCodeConfig; public ScreenController( List flowConfigurations, @@ -79,7 +82,9 @@ public ScreenController( ConditionManager conditionManager, ActionManager actionManager, FileValidationService fileValidationService, - MessageSource messageSource) { + MessageSource messageSource, + ShortCodeConfig shortCodeConfig + ) { super(submissionRepositoryService, userFileRepositoryService, flowConfigurations, formFlowConfigurationProperties, messageSource); this.validationService = validationService; @@ -87,6 +92,9 @@ public ScreenController( this.conditionManager = conditionManager; this.actionManager = actionManager; this.fileValidationService = fileValidationService; + this.submissionRepositoryService = submissionRepositoryService; + this.shortCodeConfig = shortCodeConfig; + log.info("Screen Controller Created!"); } @@ -339,10 +347,19 @@ private ModelAndView handlePost( ) ); submission.setSubmittedAt(OffsetDateTime.now()); + + if (shortCodeConfig.isCreateShortCodeAtSubmission()) { + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + } } actionManager.handleBeforeSaveAction(currentScreen, submission); submission = saveToRepository(submission); + + if (shortCodeConfig.isCreateShortCodeAtCreation()) { + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + } + setSubmissionInSession(httpSession, submission, flow); actionManager.handleAfterSaveAction(currentScreen, submission); @@ -522,6 +539,11 @@ RedirectView postSubflowScreen( actionManager.handleBeforeSaveAction(currentScreen, submission, iterationUuid); submission = saveToRepository(submission, subflowName); + + if (shortCodeConfig.isCreateShortCodeAtCreation()) { + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + } + setSubmissionInSession(httpSession, submission, flow); actionManager.handleAfterSaveAction(currentScreen, submission, iterationUuid); diff --git a/src/main/java/formflow/library/config/submission/ShortCodeConfig.java b/src/main/java/formflow/library/config/submission/ShortCodeConfig.java new file mode 100644 index 000000000..74aced5c9 --- /dev/null +++ b/src/main/java/formflow/library/config/submission/ShortCodeConfig.java @@ -0,0 +1,44 @@ +package formflow.library.config.submission; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Getter +@Configuration +public class ShortCodeConfig { + + public enum ShortCodeType { + alphanumeric, alpha, numeric; + } + + public enum ShortCodeCreationPoint { + creation, submission + } + + @Value("${form-flow.short-code.length:6}") + private int codeLength; + + @Value("${form-flow.short-code.type:alphanumeric}") + private ShortCodeType codeType; + + @Value("${form-flow.short-code.uppercase: true}") + private boolean uppercase; + + @Value("${form-flow.short-code.creation-point:submission}") + private ShortCodeCreationPoint creationPoint; + + @Value("${form-flow.short-code.prefix:#{null}}") + private String prefix; + + @Value("${form-flow.short-code.suffix:#{null}}") + private String suffix; + + public boolean isCreateShortCodeAtCreation() { + return ShortCodeCreationPoint.creation.equals(creationPoint); + } + + public boolean isCreateShortCodeAtSubmission() { + return ShortCodeCreationPoint.submission.equals(creationPoint); + } +} diff --git a/src/main/java/formflow/library/data/Submission.java b/src/main/java/formflow/library/data/Submission.java index 0eefee46a..f15ba19d8 100644 --- a/src/main/java/formflow/library/data/Submission.java +++ b/src/main/java/formflow/library/data/Submission.java @@ -9,16 +9,15 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; - import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.Optional; - +import java.util.UUID; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -76,6 +75,10 @@ public class Submission { @Column(name = "submitted_at") private OffsetDateTime submittedAt; + @Setter(AccessLevel.NONE) + @Column(name = "short_code") + private String shortCode; + /** * The key name for the field in an iteration's data that holds the status of completion for the iteration. */ @@ -220,6 +223,8 @@ public static Submission copySubmission(Submission origSubmission) { newSubmission.setSubmittedAt(origSubmission.getSubmittedAt()); newSubmission.setId(origSubmission.getId()); + newSubmission.setShortCode(origSubmission.getShortCode()); + // deep copy the subflows and any lists newSubmission.setInputData(copyMap(origSubmission.getInputData())); return newSubmission; @@ -251,6 +256,13 @@ private static Map copyMap(Map origMap) { return result; } + public void setShortCode(String shortCode) { + if (this.shortCode != null) { + throw new UnsupportedOperationException("Cannot change shortCode for an existing submission"); + } + this.shortCode = shortCode; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/formflow/library/data/SubmissionRepository.java b/src/main/java/formflow/library/data/SubmissionRepository.java index 2e47f3f13..27c981aed 100644 --- a/src/main/java/formflow/library/data/SubmissionRepository.java +++ b/src/main/java/formflow/library/data/SubmissionRepository.java @@ -1,5 +1,6 @@ package formflow.library.data; +import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -10,4 +11,7 @@ @Repository public interface SubmissionRepository extends JpaRepository { + boolean existsByShortCode(String shortCode); + + Optional findSubmissionByShortCode(String shortCode); } diff --git a/src/main/java/formflow/library/data/SubmissionRepositoryService.java b/src/main/java/formflow/library/data/SubmissionRepositoryService.java index a7115eef6..9ae7df026 100644 --- a/src/main/java/formflow/library/data/SubmissionRepositoryService.java +++ b/src/main/java/formflow/library/data/SubmissionRepositoryService.java @@ -1,6 +1,8 @@ package formflow.library.data; +import formflow.library.config.submission.ShortCodeConfig; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,67 +19,140 @@ @Slf4j public class SubmissionRepositoryService { - SubmissionRepository repository; - - SubmissionEncryptionService encryptionService; - - public SubmissionRepositoryService(SubmissionRepository repository, SubmissionEncryptionService encryptionService) { - this.repository = repository; - this.encryptionService = encryptionService; - } - - /** - * Saves the Submission in the database. - * - * @param submission the {@link formflow.library.data.Submission} to save, not null - * @return the saved {@link formflow.library.data.Submission} - */ - public Submission save(Submission submission) { - var newRecord = submission.getId() == null; - Submission savedSubmission = repository.save(encryptionService.encrypt(submission)); - if (newRecord) { - log.info("created submission id: " + savedSubmission.getId()); + SubmissionRepository repository; + + SubmissionEncryptionService encryptionService; + + ShortCodeConfig shortCodeConfig; + + public SubmissionRepositoryService(SubmissionRepository repository, SubmissionEncryptionService encryptionService, + ShortCodeConfig shortCodeConfig) { + this.repository = repository; + this.encryptionService = encryptionService; + this.shortCodeConfig = shortCodeConfig; + } + + /** + * Saves the Submission in the database. + * + * @param submission the {@link formflow.library.data.Submission} to save, not null + * @return the saved {@link formflow.library.data.Submission} + */ + public Submission save(Submission submission) { + var newRecord = submission.getId() == null; + Submission savedSubmission = repository.save(encryptionService.encrypt(submission)); + if (newRecord) { + log.info("created submission id: " + savedSubmission.getId()); + } + // straight from the db will be encrypted, so decrypt first. + return encryptionService.decrypt(savedSubmission); + } + + /** + * Searches for a particular Submission by its {@code id} + * + * @param id id of submission to look for, not null + * @return Optional containing Submission if found, else empty + */ + public Optional findById(UUID id) { + Optional submission = repository.findById(id); + return submission.map(value -> encryptionService.decrypt(value)); + } + + public Optional findByShortCode(String shortCode) { + Optional submission = repository.findSubmissionByShortCode(shortCode); + return submission.map(value -> encryptionService.decrypt(value)); + } + + /** + * Removes the CSRF from the Submission's input data, if found. + * + * @param submission submission to remove the CSRF from, not null + */ + public void removeFlowCSRF(Submission submission) { + submission.getInputData().remove("_csrf"); } - // straight from the db will be encrypted, so decrypt first. - return encryptionService.decrypt(savedSubmission); - } - - /** - * Searches for a particular Submission by its {@code id} - * - * @param id id of submission to look for, not null - * @return Optional containing Submission if found, else empty - */ - public Optional findById(UUID id) { - Optional submission = repository.findById(id); - return submission.map(value -> encryptionService.decrypt(value)); - } - - /** - * Removes the CSRF from the Submission's input data, if found. - * - * @param submission submission to remove the CSRF from, not null - */ - public void removeFlowCSRF(Submission submission) { - submission.getInputData().remove("_csrf"); - } - - /** - * Removes the CSRF from a particular Submission's Subflow's iteration data, if found. - *

- * This will remove the CSRF from all the iterations in the subflow. - *

- * - * @param submission submission to look for subflows in, not null - * @param subflowName the subflow to remove the CSRF from, not null - */ - public void removeSubflowCSRF(Submission submission, String subflowName) { - var subflowArr = (ArrayList>) submission.getInputData().get(subflowName); - - if (subflowArr != null) { - for (var entry : subflowArr) { - entry.remove("_csrf"); - } + + /** + * Removes the CSRF from a particular Submission's Subflow's iteration data, if found. + *

+ * This will remove the CSRF from all the iterations in the subflow. + *

+ * + * @param submission submission to look for subflows in, not null + * @param subflowName the subflow to remove the CSRF from, not null + */ + public void removeSubflowCSRF(Submission submission, String subflowName) { + var subflowArr = (ArrayList>) submission.getInputData().get(subflowName); + + if (subflowArr != null) { + for (var entry : subflowArr) { + entry.remove("_csrf"); + } + } + } + + /** + * generateAndSetUniqueShortCode generates a read-only unique code for the submission. The short code generation is + * configurable via {@link formflow.library.config.submission.ShortCodeConfig}: + *

+ * length (default = 6) + *

+ * characterset (alphanumeric, numeric, alpha | default = alphanumeric) + *

+ * forced uppercasing (true, false | default = true) + *

+ * creation point in {@link formflow.library.config.submission.ShortCodeConfig} (creation, submission | default = submission) + *

+ * prefix (default = null) + *

+ * suffix (default = null) + *

+ * This method will check if the generated code exists in the database, and keep trying to create a unique code, before + * persisting and returning the newly generated code-- therefore it is very important to ensure the configuration allows for a + * suitably large set of possible codes for the application. + * + * @param submission the {@link formflow.library.ScreenController} for which the short code will be generated and saved + */ + public synchronized void generateAndSetUniqueShortCode(Submission submission) { + + if (submission.getShortCode() != null) { + log.warn("Unable to create short code for submission {}", submission.getId()); + return; + } + + log.info("Attempting to create short code for submission {}", submission.getId()); + + // If there is no short code for this submission in the database, generate one + int codeLength = shortCodeConfig.getCodeLength(); + do { + String newCode = switch (shortCodeConfig.getCodeType()) { + case alphanumeric -> RandomStringUtils.randomAlphanumeric(codeLength); + case alpha -> RandomStringUtils.randomAlphabetic(codeLength); + case numeric -> RandomStringUtils.randomNumeric(codeLength); + }; + + if (shortCodeConfig.isUppercase()) { + newCode = newCode.toUpperCase(); + } + + if (shortCodeConfig.getPrefix() != null) { + newCode = shortCodeConfig.getPrefix() + newCode; + } + + if (shortCodeConfig.getSuffix() != null) { + newCode = newCode + shortCodeConfig.getSuffix(); + } + + boolean exists = repository.existsByShortCode(newCode); + if (!exists) { + // If the newly generated code isn't already in the database being used by a prior submission + // set this submission's shortcode, and persist it + submission.setShortCode(newCode); + save(submission); + } else { + log.warn("Confirmation code {} already exists", newCode); + } + } while (submission.getShortCode() == null); } - } } diff --git a/src/main/resources/db/migration/V8__submission_add_column_short_code.sql b/src/main/resources/db/migration/V8__submission_add_column_short_code.sql new file mode 100644 index 000000000..c5dd0c54e --- /dev/null +++ b/src/main/resources/db/migration/V8__submission_add_column_short_code.sql @@ -0,0 +1,2 @@ +alter table submissions + add column short_code VARCHAR NULL UNIQUE; diff --git a/src/test/java/formflow/library/config/ShortCodeConfigAlphaTest.java b/src/test/java/formflow/library/config/ShortCodeConfigAlphaTest.java new file mode 100644 index 000000000..e35349824 --- /dev/null +++ b/src/test/java/formflow/library/config/ShortCodeConfigAlphaTest.java @@ -0,0 +1,69 @@ +package formflow.library.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import formflow.library.config.submission.ShortCodeConfig; +import formflow.library.data.Submission; +import formflow.library.data.SubmissionRepositoryService; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest(properties = { + "form-flow.path=flows-config/test-flow.yaml", + "form-flow.short-code.length=5", + "form-flow.short-code.type=alpha", + "form-flow.short-code.suffix=-TEST" +}) + +class ShortCodeConfigAlphaTest { + + @Autowired + private SubmissionRepositoryService submissionRepositoryService; + + @Autowired + private ShortCodeConfig shortCodeConfig; + + @Test + void testShortCodeGeneration_Numeric() { + Submission submission = new Submission(); + submission.setFlow("testFlow"); + submission = saveAndReload(submission); + + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + + int expectedLength = shortCodeConfig.getCodeLength() + + (shortCodeConfig.getPrefix() != null ? shortCodeConfig.getPrefix().length() : 0) + + (shortCodeConfig.getSuffix() != null ? shortCodeConfig.getSuffix().length() : 0); + + assertThat(submission.getShortCode().length()).isEqualTo(expectedLength); + + String coreOfCode = submission.getShortCode(); + if (shortCodeConfig.getPrefix() != null) { + coreOfCode = submission.getShortCode().substring(shortCodeConfig.getPrefix().length()); + } + + if (shortCodeConfig.getSuffix() != null) { + coreOfCode = coreOfCode.substring(0, coreOfCode.length() - shortCodeConfig.getSuffix().length()); + } + + assertThat(coreOfCode.matches("[A-Za-z]+")).isEqualTo(true); + + Optional reloadedSubmission = submissionRepositoryService.findByShortCode(submission.getShortCode()); + if (reloadedSubmission.isPresent()) { + assertThat(submission).isEqualTo(reloadedSubmission.get()); + assertThat(submission.getShortCode()).isEqualTo(reloadedSubmission.get().getShortCode()); + } else { + Assertions.fail(); + } + } + + private Submission saveAndReload(Submission submission) { + Submission savedSubmission = submissionRepositoryService.save(submission); + return submissionRepositoryService.findById(savedSubmission.getId()).orElseThrow(); + } +} diff --git a/src/test/java/formflow/library/config/ShortCodeConfigNumericTest.java b/src/test/java/formflow/library/config/ShortCodeConfigNumericTest.java new file mode 100644 index 000000000..1d571bff7 --- /dev/null +++ b/src/test/java/formflow/library/config/ShortCodeConfigNumericTest.java @@ -0,0 +1,67 @@ +package formflow.library.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import formflow.library.config.submission.ShortCodeConfig; +import formflow.library.data.Submission; +import formflow.library.data.SubmissionRepositoryService; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest(properties = { + "form-flow.path=flows-config/test-flow.yaml", + "form-flow.short-code.length=7", + "form-flow.short-code.type=numeric" +}) +class ShortCodeConfigNumericTest { + + @Autowired + private SubmissionRepositoryService submissionRepositoryService; + + @Autowired + private ShortCodeConfig shortCodeConfig; + + @Test + void testShortCodeGeneration_Numeric() { + Submission submission = new Submission(); + submission.setFlow("testFlow"); + submission = saveAndReload(submission); + + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + + int expectedLength = shortCodeConfig.getCodeLength() + + (shortCodeConfig.getPrefix() != null ? shortCodeConfig.getPrefix().length() : 0) + + (shortCodeConfig.getSuffix() != null ? shortCodeConfig.getSuffix().length() : 0); + + assertThat(submission.getShortCode().length()).isEqualTo(expectedLength); + + String coreOfCode = submission.getShortCode(); + if (shortCodeConfig.getPrefix() != null) { + coreOfCode = submission.getShortCode().substring(shortCodeConfig.getPrefix().length()); + } + + if (shortCodeConfig.getSuffix() != null) { + coreOfCode = coreOfCode.substring(0, coreOfCode.length() - shortCodeConfig.getSuffix().length()); + } + + assertThat(coreOfCode.matches("[0-9]+")).isEqualTo(true); + + Optional reloadedSubmission = submissionRepositoryService.findByShortCode(submission.getShortCode()); + if (reloadedSubmission.isPresent()) { + assertThat(submission).isEqualTo(reloadedSubmission.get()); + assertThat(submission.getShortCode()).isEqualTo(reloadedSubmission.get().getShortCode()); + } else { + Assertions.fail(); + } + } + + private Submission saveAndReload(Submission submission) { + Submission savedSubmission = submissionRepositoryService.save(submission); + return submissionRepositoryService.findById(savedSubmission.getId()).orElseThrow(); + } +} diff --git a/src/test/java/formflow/library/config/ShortCodeConfigUpperAlphaTest.java b/src/test/java/formflow/library/config/ShortCodeConfigUpperAlphaTest.java new file mode 100644 index 000000000..910427561 --- /dev/null +++ b/src/test/java/formflow/library/config/ShortCodeConfigUpperAlphaTest.java @@ -0,0 +1,67 @@ +package formflow.library.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import formflow.library.config.submission.ShortCodeConfig; +import formflow.library.data.Submission; +import formflow.library.data.SubmissionRepositoryService; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest(properties = { + "form-flow.path=flows-config/test-flow.yaml", + "form-flow.short-code.length=5", + "form-flow.short-code.type=alpha", + "form-flow.short-code.uppercase=true" +}) +class ShortCodeConfigUpperAlphaTest { + + @Autowired + private SubmissionRepositoryService submissionRepositoryService; + + @Autowired + private ShortCodeConfig shortCodeConfig; + + @Test + void testShortCodeGeneration_Numeric() { + Submission submission = new Submission(); + submission.setFlow("testFlow"); + submission = saveAndReload(submission); + + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + + int expectedLength = shortCodeConfig.getCodeLength() + + (shortCodeConfig.getPrefix() != null ? shortCodeConfig.getPrefix().length() : 0) + + (shortCodeConfig.getSuffix() != null ? shortCodeConfig.getSuffix().length() : 0); + + assertThat(submission.getShortCode().length()).isEqualTo(expectedLength); + + String coreOfCode = submission.getShortCode(); + if (shortCodeConfig.getPrefix() != null) { + coreOfCode = submission.getShortCode().substring(shortCodeConfig.getPrefix().length()); + } + + if (shortCodeConfig.getSuffix() != null) { + coreOfCode = coreOfCode.substring(0, coreOfCode.length() - shortCodeConfig.getSuffix().length()); + } + assertThat(coreOfCode.matches("[A-Z]+")).isEqualTo(true); + + Optional reloadedSubmission = submissionRepositoryService.findByShortCode(submission.getShortCode()); + if (reloadedSubmission.isPresent()) { + assertThat(submission).isEqualTo(reloadedSubmission.get()); + assertThat(submission.getShortCode()).isEqualTo(reloadedSubmission.get().getShortCode()); + } else { + Assertions.fail(); + } + } + + private Submission saveAndReload(Submission submission) { + Submission savedSubmission = submissionRepositoryService.save(submission); + return submissionRepositoryService.findById(savedSubmission.getId()).orElseThrow(); + } +} diff --git a/src/test/java/formflow/library/controllers/LockedSubmissionRedirectTest.java b/src/test/java/formflow/library/controllers/LockedSubmissionRedirectTest.java index 892039c15..8c3e663f8 100644 --- a/src/test/java/formflow/library/controllers/LockedSubmissionRedirectTest.java +++ b/src/test/java/formflow/library/controllers/LockedSubmissionRedirectTest.java @@ -38,474 +38,489 @@ @SpringBootTest(properties = {"form-flow.path=flows-config/test-flow.yaml"}) @TestPropertySource(properties = { - "form-flow.lock-after-submitted[0].flow=testFlow", - "form-flow.lock-after-submitted[0].redirect=success" + "form-flow.lock-after-submitted[0].flow=testFlow", + "form-flow.lock-after-submitted[0].redirect=success" }) public class LockedSubmissionRedirectTest extends AbstractMockMvcTest { - @MockBean - private UserFileRepositoryService userFileRepositoryService; - - @BeforeEach - public void setUp() throws Exception { - UUID submissionUUID = UUID.randomUUID(); - submission = Submission.builder().id(submissionUUID).urlParams(new HashMap<>()).inputData(new HashMap<>()).build(); - // this sets up flow info in the session to get passed along later on. - setFlowInfoInSession(session, "testFlow", submission.getId()); - super.setUp(); - } - - @Test - public void shouldRedirectWhenAttemptingToGetAPageThatIsNotAllowedForAFlowWithALockedSubmission() throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/inputs") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInput", List.of("firstFlowTextInputValue"), - "numberInput", List.of("10")))) - ); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + @MockBean + private UserFileRepositoryService userFileRepositoryService; + + @BeforeEach + public void setUp() throws Exception { + UUID submissionUUID = UUID.randomUUID(); + submission = Submission.builder().id(submissionUUID).urlParams(new HashMap<>()).inputData(new HashMap<>()).build(); + // this sets up flow info in the session to get passed along later on. + setFlowInfoInSession(session, "testFlow", submission.getId()); + super.setUp(); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - // Assert that we are redirected to the configured screen when we try to go back into the flow when the submission should be locked - mockMvc.perform(get("/flow/testFlow/inputs") - .session(session)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - } - - @Test - void shouldRedirectToConfiguredScreenWhenPostingDataToAFlowThatHasBeenConfiguredToLockAfterBeingSubmittedWithoutUpdatingSubmissionObject() - throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/inputs") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInput", List.of("firstFlowTextInputValue"), - "numberInput", List.of("10")))) - ); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + public void shouldRedirectWhenAttemptingToGetAPageThatIsNotAllowedForAFlowWithALockedSubmission() throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/inputs") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInput", List.of("firstFlowTextInputValue"), + "numberInput", List.of("10")))) + ); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + // Assert that we are redirected to the configured screen when we try to go back into the flow when the submission should be locked + mockMvc.perform(get("/flow/testFlow/inputs") + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - // Post again and assert that the submission is not updated - mockMvc.perform(post("/flow/testFlow/inputs") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInput", List.of("newValue"), - "numberInput", List.of("333"), - "moneyInput", List.of("444"))))) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - - Optional testFlowSubmissionAfterAttemptingToPostAfterSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getSubmittedAt()).isNotNull(); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData().get("textInput")).isEqualTo( - "firstFlowTextInputValue"); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData().get("numberInput")).isEqualTo("10"); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData().get("moneyInput")).isNull(); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData() - .equals(testFlowSubmissionAfterBeingSubmitted.get().getInputData())).isTrue(); - } - - @Test - void shouldRedirectWhenAttemptingToGetAScreenInsideASubflowThatIsNotAllowedForAFlowWithALockedSubmission() throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflow", List.of("textInputValue"), - "numberInputSubflow", List.of("10")))) - ); - - // Get the UUID for the iteration we just created - UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); - Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); - List> subflowIterations = (List>) submissionBeforeSubflowIsCompleted.getInputData() - .get("testSubflow"); - String uuidString = (String) subflowIterations.get(0).get("uuid"); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + void shouldRedirectToConfiguredScreenWhenPostingDataToAFlowThatHasBeenConfiguredToLockAfterBeingSubmittedWithoutUpdatingSubmissionObject() + throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/inputs") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInput", List.of("firstFlowTextInputValue"), + "numberInput", List.of("10")))) + ); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + // Post again and assert that the submission is not updated + mockMvc.perform(post("/flow/testFlow/inputs") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInput", List.of("newValue"), + "numberInput", List.of("333"), + "moneyInput", List.of("444"))))) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); + + Optional testFlowSubmissionAfterAttemptingToPostAfterSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData().get("textInput")).isEqualTo( + "firstFlowTextInputValue"); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData().get("numberInput")).isEqualTo("10"); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData().get("moneyInput")).isNull(); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getInputData() + .equals(testFlowSubmissionAfterBeingSubmitted.get().getInputData())).isTrue(); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - // Assert that we are redirected to the configured screen when we try to go back into the flow when the submission should be locked - mockMvc.perform(get("/flow/testFlow/subflowAddItem/" + uuidString) - .session(session)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - - // Assert the same as above but for the /edit endpoint - mockMvc.perform(get("/flow/testFlow/subflowAddItem/" + uuidString + "/edit") - .session(session)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - } - - @Test - void shouldRedirectToConfiguredScreenWhenPostingSubflowDataToAFlowThatHasBeenConfiguredToLockAfterBeingSubmittedWithoutUpdatingSubmissionObject() - throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflow", List.of("textInputValue"), - "numberInputSubflow", List.of("10")))) - ); - - // Get the UUID for the iteration we just created - UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); - Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); - List> subflowIterationsBeforeSubmit = (List>) submissionBeforeSubflowIsCompleted.getInputData() - .get("testSubflow"); - String uuidString = (String) subflowIterationsBeforeSubmit.get(0).get("uuid"); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - - // Submit the flow, assert we reach the success page - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + void shouldRedirectWhenAttemptingToGetAScreenInsideASubflowThatIsNotAllowedForAFlowWithALockedSubmission() throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflow", List.of("textInputValue"), + "numberInputSubflow", List.of("10")))) + ); + + // Get the UUID for the iteration we just created + UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); + Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); + List> subflowIterations = (List>) submissionBeforeSubflowIsCompleted.getInputData() + .get("testSubflow"); + String uuidString = (String) subflowIterations.get(0).get("uuid"); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + // Assert that we are redirected to the configured screen when we try to go back into the flow when the submission should be locked + mockMvc.perform(get("/flow/testFlow/subflowAddItem/" + uuidString) + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); + + // Assert the same as above but for the /edit endpoint + mockMvc.perform(get("/flow/testFlow/subflowAddItem/" + uuidString + "/edit") + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - // Post again and assert that the submission is not updated and that we are redirected - mockMvc.perform(post("/flow/testFlow/subflowAddItem/" + uuidString) - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflow", List.of("newValue"), - "numberInputSubflow", List.of("333"), - "moneyInputSubflow", List.of("444"))))) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - - Optional testFlowSubmissionAfterAttemptingToPostAfterSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getSubmittedAt()).isNotNull(); - - List> subflowIterationsAfterSubmit = (List>) testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get() - .getInputData() - .get("testSubflow"); - Map subflowIteration = subflowIterationsAfterSubmit.get(0); - assertThat(subflowIteration.get("textInputSubflow")).isEqualTo("textInputValue"); - assertThat(subflowIteration.get("numberInputSubflow")).isEqualTo("10"); - assertThat(subflowIteration.get("moneyInputSubflow")).isNull(); - assertThat(subflowIterationsAfterSubmit.equals(subflowIterationsBeforeSubmit)).isTrue(); - - // Do the same but for the /edit endpoint - mockMvc.perform(post("/flow/testFlow/subflowAddItem/" + uuidString + "/edit") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflow", List.of("editValue"), - "numberInputSubflow", List.of("111"), - "moneyInputSubflow", List.of("222"))))) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - - Optional testFlowSubmissionAfterAttemptingToEditAfterSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterAttemptingToEditAfterSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterAttemptingToEditAfterSubmitted.get().getSubmittedAt()).isNotNull(); - List> subflowIterationsAfterEdit = (List>) testFlowSubmissionAfterAttemptingToEditAfterSubmitted.get() - .getInputData() - .get("testSubflow"); - Map subflowIterationAfterEdit = subflowIterationsAfterEdit.get(0); - assertThat(subflowIterationAfterEdit.get("textInputSubflow")).isEqualTo("textInputValue"); - assertThat(subflowIterationAfterEdit.get("numberInputSubflow")).isEqualTo("10"); - assertThat(subflowIterationAfterEdit.get("moneyInputSubflow")).isNull(); - assertThat(subflowIterationsAfterEdit.equals(subflowIterationsBeforeSubmit)).isTrue(); - } - - @Test - void shouldRedirectWhenAttemptingToGetTheSubflowDeleteConfirmationScreenForAFlowWithALockedSubmission() throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflow", List.of("textInputValue"), - "numberInputSubflow", List.of("10")))) - ); - - // Get the UUID for the iteration we just created - UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); - Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); - List> subflowIterationsBeforeSubmit = (List>) submissionBeforeSubflowIsCompleted.getInputData() - .get("testSubflow"); - String uuidString = (String) subflowIterationsBeforeSubmit.get(0).get("uuid"); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + void shouldRedirectToConfiguredScreenWhenPostingSubflowDataToAFlowThatHasBeenConfiguredToLockAfterBeingSubmittedWithoutUpdatingSubmissionObject() + throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflow", List.of("textInputValue"), + "numberInputSubflow", List.of("10")))) + ); + + // Get the UUID for the iteration we just created + UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); + Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); + List> subflowIterationsBeforeSubmit = (List>) submissionBeforeSubflowIsCompleted.getInputData() + .get("testSubflow"); + String uuidString = (String) subflowIterationsBeforeSubmit.get(0).get("uuid"); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + // Submit the flow, assert we reach the success page + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + // Post again and assert that the submission is not updated and that we are redirected + mockMvc.perform(post("/flow/testFlow/subflowAddItem/" + uuidString) + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflow", List.of("newValue"), + "numberInputSubflow", List.of("333"), + "moneyInputSubflow", List.of("444"))))) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); + + Optional testFlowSubmissionAfterAttemptingToPostAfterSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get().getSubmittedAt()).isNotNull(); + + List> subflowIterationsAfterSubmit = (List>) testFlowSubmissionAfterAttemptingToPostAfterSubmitted.get() + .getInputData() + .get("testSubflow"); + Map subflowIteration = subflowIterationsAfterSubmit.get(0); + assertThat(subflowIteration.get("textInputSubflow")).isEqualTo("textInputValue"); + assertThat(subflowIteration.get("numberInputSubflow")).isEqualTo("10"); + assertThat(subflowIteration.get("moneyInputSubflow")).isNull(); + assertThat(subflowIterationsAfterSubmit.equals(subflowIterationsBeforeSubmit)).isTrue(); + + // Do the same but for the /edit endpoint + mockMvc.perform(post("/flow/testFlow/subflowAddItem/" + uuidString + "/edit") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflow", List.of("editValue"), + "numberInputSubflow", List.of("111"), + "moneyInputSubflow", List.of("222"))))) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); + + Optional testFlowSubmissionAfterAttemptingToEditAfterSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterAttemptingToEditAfterSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterAttemptingToEditAfterSubmitted.get().getSubmittedAt()).isNotNull(); + List> subflowIterationsAfterEdit = (List>) testFlowSubmissionAfterAttemptingToEditAfterSubmitted.get() + .getInputData() + .get("testSubflow"); + Map subflowIterationAfterEdit = subflowIterationsAfterEdit.get(0); + assertThat(subflowIterationAfterEdit.get("textInputSubflow")).isEqualTo("textInputValue"); + assertThat(subflowIterationAfterEdit.get("numberInputSubflow")).isEqualTo("10"); + assertThat(subflowIterationAfterEdit.get("moneyInputSubflow")).isNull(); + assertThat(subflowIterationsAfterEdit.equals(subflowIterationsBeforeSubmit)).isTrue(); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - // Assert that we are redirected to the configured screen when we try to the subflow delete confirmation screen - mockMvc.perform(get("/flow/testFlow/testSubflow/" + uuidString + "/deleteConfirmation") - .session(session)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - } - - @Test - void shouldRedirectWhenAttemptingToDeleteASubflowIterationFromAFlowThatIsLocked() throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflow", List.of("textInputValue"), - "numberInputSubflow", List.of("10")))) - ); - - // Get the UUID for the iteration we just created - UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); - Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); - List> subflowIterationsBeforeSubmit = (List>) submissionBeforeSubflowIsCompleted.getInputData() - .get("testSubflow"); - String uuidString = (String) subflowIterationsBeforeSubmit.get(0).get("uuid"); - - // Complete the subflow so we get a completed iteration - ResultActions iterationResult = mockMvc.perform(post("/flow/testFlow/subflowAddItemPage2/" + uuidString) - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInputSubflowPage2", List.of("newValue"), - "moneyInputSubflowPage2", List.of("444"))))); - - // We need to hit the navigation endpoint because that is where we set the iterationIsComplete flag - String screenAfterSubflow = "/flow/testFlow/subflowAddItemPage2/navigation?uuid=" + uuidString; - iterationResult.andExpect(redirectedUrl(screenAfterSubflow)); - - while (Objects.requireNonNull(screenAfterSubflow).contains("/navigation")) { - // follow redirects - screenAfterSubflow = mockMvc.perform(get(screenAfterSubflow).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + void shouldRedirectWhenAttemptingToGetTheSubflowDeleteConfirmationScreenForAFlowWithALockedSubmission() throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflow", List.of("textInputValue"), + "numberInputSubflow", List.of("10")))) + ); + + // Get the UUID for the iteration we just created + UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); + Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); + List> subflowIterationsBeforeSubmit = (List>) submissionBeforeSubflowIsCompleted.getInputData() + .get("testSubflow"); + String uuidString = (String) subflowIterationsBeforeSubmit.get(0).get("uuid"); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + // Assert that we are redirected to the configured screen when we try to the subflow delete confirmation screen + mockMvc.perform(get("/flow/testFlow/testSubflow/" + uuidString + "/deleteConfirmation") + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); } - assertThat(screenAfterSubflow).isEqualTo("/flow/testFlow/testReviewScreen"); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - // Assert the subflow iteration is complete - List> subflowIterationsBeforeSubmitting = (List>) testFlowSubmission.get() - .getInputData() - .get("testSubflow"); - Map subflowIteration = subflowIterationsBeforeSubmitting.get(0); - assertThat(subflowIteration.get("iterationIsComplete")).isEqualTo(true); - - // Submit the flow, assert we reach the success page - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + void shouldRedirectWhenAttemptingToDeleteASubflowIterationFromAFlowThatIsLocked() throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/subflowAddItem/new") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflow", List.of("textInputValue"), + "numberInputSubflow", List.of("10")))) + ); + + // Get the UUID for the iteration we just created + UUID testSubflowLogicUUID = ((Map) session.getAttribute(SUBMISSION_MAP_NAME)).get("testFlow"); + Submission submissionBeforeSubflowIsCompleted = submissionRepositoryService.findById(testSubflowLogicUUID).get(); + List> subflowIterationsBeforeSubmit = (List>) submissionBeforeSubflowIsCompleted.getInputData() + .get("testSubflow"); + String uuidString = (String) subflowIterationsBeforeSubmit.get(0).get("uuid"); + + // Complete the subflow so we get a completed iteration + ResultActions iterationResult = mockMvc.perform(post("/flow/testFlow/subflowAddItemPage2/" + uuidString) + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInputSubflowPage2", List.of("newValue"), + "moneyInputSubflowPage2", List.of("444"))))); + + // We need to hit the navigation endpoint because that is where we set the iterationIsComplete flag + String screenAfterSubflow = "/flow/testFlow/subflowAddItemPage2/navigation?uuid=" + uuidString; + iterationResult.andExpect(redirectedUrl(screenAfterSubflow)); + + while (Objects.requireNonNull(screenAfterSubflow).contains("/navigation")) { + // follow redirects + screenAfterSubflow = mockMvc.perform(get(screenAfterSubflow).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(screenAfterSubflow).isEqualTo("/flow/testFlow/testReviewScreen"); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + // Assert the subflow iteration is complete + List> subflowIterationsBeforeSubmitting = (List>) testFlowSubmission.get() + .getInputData() + .get("testSubflow"); + Map subflowIteration = subflowIterationsBeforeSubmitting.get(0); + assertThat(subflowIteration.get("iterationIsComplete")).isEqualTo(true); + + // Submit the flow, assert we reach the success page + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + // Assert that we are redirected when attempting to delete a subflow + mockMvc.perform(post("/flow/testFlow/testSubflow/" + uuidString + "/delete") + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - // Assert that we are redirected when attempting to delete a subflow - mockMvc.perform(post("/flow/testFlow/testSubflow/" + uuidString + "/delete") - .session(session)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirect -> assertEquals("/flow/testFlow/success", - Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); - } - - @Test - void shouldErrorWhenAttemptingToUploadFilesToAFlowWithALockedSubmission() throws Exception { - // Make an initial post to create the submission and give it some data - mockMvc.perform(post("/flow/testFlow/inputs") - .session(session) - .params(new LinkedMultiValueMap<>(Map.of( - "textInput", List.of("firstFlowTextInputValue"), - "numberInput", List.of("10")))) - ); - - // Assert that the submissions submittedAt value is null before submitting - Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); - Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); - assertThat(testFlowSubmission.isPresent()).isTrue(); - assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); - - ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); - String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; - result.andExpect(redirectedUrl(nextScreenUrl)); - - while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { - // follow redirects - nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) - .andExpect(status().is3xxRedirection()).andReturn() - .getResponse() - .getRedirectedUrl(); + + @Test + void shouldErrorWhenAttemptingToUploadFilesToAFlowWithALockedSubmission() throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/inputs") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInput", List.of("firstFlowTextInputValue"), + "numberInput", List.of("10")))) + ); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); + assertThat(testFlowSubmission.get().getShortCode()).isNull(); + + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + UUID fileId = UUID.randomUUID(); + + MockMultipartFile testImage = new MockMultipartFile("file", "someImage.jpg", + MediaType.IMAGE_JPEG_VALUE, "test".getBytes()); + + when(userFileRepositoryService.save(any())).thenAnswer(invocation -> { + UserFile userFile = invocation.getArgument(0); + userFile.setFileId(fileId); + return userFile; + }); + + ResultActions resultAfterSubmit = mockMvc.perform(MockMvcRequestBuilders.multipart("/file-upload") + .file(testImage) + .param("flow", "testFlow") + .param("screen", "testUpload") + .param("inputName", "testUpload") + .param("thumbDataURL", "base64string") + .session(session) + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) + .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); + + assertThat(resultAfterSubmit.andReturn().getResponse().getContentAsString()).contains( + "You've already submitted this application. You can no longer upload documents at this time."); } - assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); - FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); - assertThat(nextScreen.getTitle()).isEqualTo("Success"); - - // Assert that the submissions submittedAt value is not null after submitting - Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( - submissionMap.get("testFlow")); - assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); - assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); - - UUID fileId = UUID.randomUUID(); - - MockMultipartFile testImage = new MockMultipartFile("file", "someImage.jpg", - MediaType.IMAGE_JPEG_VALUE, "test".getBytes()); - - when(userFileRepositoryService.save(any())).thenAnswer(invocation -> { - UserFile userFile = invocation.getArgument(0); - userFile.setFileId(fileId); - return userFile; - }); - - ResultActions resultAfterSubmit = mockMvc.perform(MockMvcRequestBuilders.multipart("/file-upload") - .file(testImage) - .param("flow", "testFlow") - .param("screen", "testUpload") - .param("inputName", "testUpload") - .param("thumbDataURL", "base64string") - .session(session) - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE)) - .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); - - assertThat(resultAfterSubmit.andReturn().getResponse().getContentAsString()).contains( - "You've already submitted this application. You can no longer upload documents at this time."); - } } diff --git a/src/test/java/formflow/library/controllers/ScreenControllerShortCodeCreationPointTest.java b/src/test/java/formflow/library/controllers/ScreenControllerShortCodeCreationPointTest.java new file mode 100644 index 000000000..147557e44 --- /dev/null +++ b/src/test/java/formflow/library/controllers/ScreenControllerShortCodeCreationPointTest.java @@ -0,0 +1,96 @@ +package formflow.library.controllers; + +import static formflow.library.FormFlowController.SUBMISSION_MAP_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import formflow.library.data.Submission; +import formflow.library.data.UserFileRepositoryService; +import formflow.library.utilities.AbstractMockMvcTest; +import formflow.library.utilities.FormScreen; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.util.LinkedMultiValueMap; + +@SpringBootTest(properties = {"form-flow.path=flows-config/test-flow.yaml"}) +@TestPropertySource(properties = { + "form-flow.lock-after-submitted[0].flow=testFlow", + "form-flow.lock-after-submitted[0].redirect=success", + "form-flow.short-code.creation-point=creation" +}) +public class ScreenControllerShortCodeCreationPointTest extends AbstractMockMvcTest { + + @MockBean + private UserFileRepositoryService userFileRepositoryService; + + @BeforeEach + public void setUp() throws Exception { + UUID submissionUUID = UUID.randomUUID(); + submission = Submission.builder().id(submissionUUID).urlParams(new HashMap<>()).inputData(new HashMap<>()).build(); + // this sets up flow info in the session to get passed along later on. + setFlowInfoInSession(session, "testFlow", submission.getId()); + super.setUp(); + } + + @Test + public void testSubmissionWithShortCodeCreatedAtCreation() throws Exception { + // Make an initial post to create the submission and give it some data + mockMvc.perform(post("/flow/testFlow/inputs") + .session(session) + .params(new LinkedMultiValueMap<>(Map.of( + "textInput", List.of("firstFlowTextInputValue"), + "numberInput", List.of("10")))) + ); + + // Assert that the submissions submittedAt value is null before submitting + Map submissionMap = (Map) session.getAttribute(SUBMISSION_MAP_NAME); + Optional testFlowSubmission = submissionRepositoryService.findById(submissionMap.get("testFlow")); + assertThat(testFlowSubmission.isPresent()).isTrue(); + assertThat(testFlowSubmission.get().getSubmittedAt()).isNull(); +// assertThat(testFlowSubmission.get().getShortCode()).isNotNull(); + + ResultActions result = mockMvc.perform(post("/flow/testFlow/pageWithCustomSubmitButton/submit").session(session)); + String nextScreenUrl = "/flow/testFlow/pageWithCustomSubmitButton/navigation"; + result.andExpect(redirectedUrl(nextScreenUrl)); + + while (Objects.requireNonNull(nextScreenUrl).contains("/navigation")) { + // follow redirects + nextScreenUrl = mockMvc.perform(get(nextScreenUrl).session(session)) + .andExpect(status().is3xxRedirection()).andReturn() + .getResponse() + .getRedirectedUrl(); + } + assertThat(nextScreenUrl).isEqualTo("/flow/testFlow/success"); + FormScreen nextScreen = new FormScreen(mockMvc.perform(get(nextScreenUrl))); + assertThat(nextScreen.getTitle()).isEqualTo("Success"); + + // Assert that the submissions submittedAt value is not null after submitting + Optional testFlowSubmissionAfterBeingSubmitted = submissionRepositoryService.findById( + submissionMap.get("testFlow")); + assertThat(testFlowSubmissionAfterBeingSubmitted.isPresent()).isTrue(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getSubmittedAt()).isNotNull(); + assertThat(testFlowSubmissionAfterBeingSubmitted.get().getShortCode()).isNotNull(); + + + // Assert that we are redirected to the configured screen when we try to go back into the flow when the submission should be locked + mockMvc.perform(get("/flow/testFlow/inputs") + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirect -> assertEquals("/flow/testFlow/success", + Objects.requireNonNull(redirect.getResponse().getRedirectedUrl()))); + } +} diff --git a/src/test/java/formflow/library/repository/SubmissionRepositoryServiceTest.java b/src/test/java/formflow/library/repository/SubmissionRepositoryServiceTest.java index afdd09490..7f8f56f2d 100644 --- a/src/test/java/formflow/library/repository/SubmissionRepositoryServiceTest.java +++ b/src/test/java/formflow/library/repository/SubmissionRepositoryServiceTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import formflow.library.config.submission.ShortCodeConfig; import formflow.library.data.Submission; import formflow.library.data.SubmissionRepositoryService; import jakarta.persistence.EntityManager; @@ -12,7 +13,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -28,6 +31,9 @@ class SubmissionRepositoryServiceTest { @PersistenceContext EntityManager entityManager; + @Autowired + private ShortCodeConfig shortCodeConfig; + @Test void shouldSaveASubmissionWithUUID() { Submission firstSubmission = new Submission(); @@ -38,6 +44,69 @@ void shouldSaveASubmissionWithUUID() { assertThat(firstSubmission.getId()).isInstanceOf(UUID.class); } + @Test + void testShortCodePersistsOneTimeOnly() { + Submission submission = new Submission(); + submission.setFlow("testFlow"); + assertThat(submission.getShortCode()).isNull(); + + submission = saveAndReload(submission); + assertThat(submission.getId()).isInstanceOf(UUID.class); + + // Only saved, not submitted via the controller so the short code should be null + assertThat(submission.getShortCode()).isNull(); + + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + Submission reloaded = submissionRepositoryService.findById(submission.getId()).get(); + assertThat(reloaded.getShortCode()).isNotNull(); + + try { + submission.setShortCode("testShortCode"); + Assertions.fail(); + } catch (UnsupportedOperationException e) { + assertThat(reloaded.getShortCode()).isEqualTo(submission.getShortCode()); + } + + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + // this should be a no-op, because there already is a Short Code + reloaded = submissionRepositoryService.findById(submission.getId()).get(); + assertThat(reloaded.getShortCode()).isEqualTo(submission.getShortCode()); + } + + @Test + void testFindByShortCode() { + Submission submission = new Submission(); + submission.setFlow("testFlow"); + submission = saveAndReload(submission); + + submissionRepositoryService.generateAndSetUniqueShortCode(submission); + + int expectedLength = shortCodeConfig.getCodeLength() + + (shortCodeConfig.getPrefix() != null ? shortCodeConfig.getPrefix().length() : 0) + + (shortCodeConfig.getSuffix() != null ? shortCodeConfig.getSuffix().length() : 0); + + assertThat(submission.getShortCode().length()).isEqualTo(expectedLength); + + String coreOfCode = submission.getShortCode(); + if (shortCodeConfig.getPrefix() != null) { + coreOfCode = submission.getShortCode().substring(shortCodeConfig.getPrefix().length()); + } + + if (shortCodeConfig.getSuffix() != null) { + coreOfCode = coreOfCode.substring(0, coreOfCode.length() - shortCodeConfig.getSuffix().length()); + } + + assertThat(coreOfCode.matches("[A-Za-z0-9]+")).isEqualTo(true); + + Optional reloadedSubmission = submissionRepositoryService.findByShortCode(submission.getShortCode()); + if (reloadedSubmission.isPresent()) { + assertThat(submission).isEqualTo(reloadedSubmission.get()); + assertThat(submission.getShortCode()).isEqualTo(reloadedSubmission.get().getShortCode()); + } else { + Assertions.fail(); + } + } + @Test void shouldSaveSubmission() { var inputData = Map.of( diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index a060239ce..617d35f56 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -27,6 +27,11 @@ form-flow: key: testing-fake-key domain: 'mail.forms-starter.cfa-platforms.org' sender-email: 'Testing ' + short-code: + length: 8 + type: alphanumeric + uppercase: false + prefix: "IL-" spring: main: allow-bean-definition-overriding: true