diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index a67bf660..da28e1d9 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -40,9 +40,10 @@ jobs: --enableRetired --disableOssIndex true --disableRetireJS true + --disableCentral true - name: Upload Test results - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: DependencyCheck report path: ${{github.workspace}}/reports diff --git a/.github/workflows/pushImage.yml b/.github/workflows/pushImage.yml index c354fe84..fefc637d 100644 --- a/.github/workflows/pushImage.yml +++ b/.github/workflows/pushImage.yml @@ -41,8 +41,9 @@ jobs: args: > --enableRetired --disableOssIndex true + --disableCentral true - name: Upload Test results - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: DependencyCheck report path: ${{github.workspace}}/reports @@ -110,7 +111,7 @@ jobs: run: docker save --output ${{ steps.docker-image-name.outputs.name }}.tar $ECR_REGISTRY/gap-apply-admin-backend:${{ env.BUILD_VERSION }} - name: Upload Docker image - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ steps.docker-image-name.outputs.name }} path: ${{ steps.docker-image-name.outputs.name }}.tar @@ -148,7 +149,7 @@ jobs: echo BUILD_VERSION is ${{ env.BUILD_VERSION }} - name: Download Docker image - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ needs.build.outputs.docker-image-name }} diff --git a/README.md b/README.md index 2d916401..5f52ae6b 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,97 @@ mvn Test -Dtest=className mvn Test -Dtest=className#methodName ``` +## Troubleshooting + +### MapStruct Mapper Implementation Not Found + +If you encounter an error like: + +``` +Parameter X of constructor in ...Service required a bean of type '...Mapper' that could not be found. +``` + +This typically means MapStruct hasn't generated the mapper implementation classes. MapStruct generates these during compilation. + +**Solution:** + +1. **Clean and rebuild the project:** + ```bash + mvn clean compile + ``` + + Or in your IDE: + - **IntelliJ IDEA**: Build → Rebuild Project + - **Eclipse**: Project → Clean → Clean all projects + +2. **Verify annotation processing is enabled:** + - **IntelliJ IDEA**: Settings → Build, Execution, Deployment → Compiler → Annotation Processors → Enable annotation processing + - **Eclipse**: Project Properties → Java Compiler → Annotation Processing → Enable annotation processing + +3. **Check for compilation errors:** If there are any compilation errors in mapper interfaces or related DTOs, MapStruct won't generate the implementation classes. Check the compiler output for errors. + +After a successful build, you should see generated mapper implementations in: +``` +target/generated-sources/annotations/gov/cabinetoffice/gap/adminbackend/mappers/ +``` + +### Flyway Migration Checksum Mismatch + +If you encounter an error during deployment like: + +``` +Migration checksum mismatch for migration version X.XX +-> Applied to database : -336000449 +-> Resolved locally : -252574045 +Either revert the changes to the migration, or run repair to update the schema history. +``` + +This occurs when a migration file has been modified after it was already applied to the database. Flyway validates checksums to ensure migration files haven't been changed. + +**Important:** Never modify migration files after they've been deployed to any environment. If you need to change something, create a new migration file instead. + +#### Solution Options + +**Option 1: Use Flyway Repair (Recommended)** + +Enable Flyway repair mode temporarily in your deployment configuration: + +```properties +spring.flyway.repair=true +``` + +Then restart the application. Flyway will repair all checksums to match the current migration files. After the repair completes, remove this property. + +**Option 2: Manually Update Checksum in Database** + +If you have direct database access, you can manually update the checksum in the `flyway_schema_history` table: + +```sql +-- Connect to your database and run: +UPDATE flyway_schema_history +SET checksum = -- Use the checksum from the error message +WHERE version = ''; +``` + +**⚠️ Warning:** Only update the checksum if you're certain the migration file changes don't affect the database schema. If the migration was modified to change the schema, you may need to create a new migration to apply those changes instead. + +**Option 3: Use Flyway CLI Repair Command** + +If you have access to run commands on your deployment environment: + +```bash +flyway repair -url=jdbc:postgresql://:5432/ \ + -user= -password= \ + -locations=filesystem:src/main/resources/db/migration +``` + +#### Prevention + +- **Never modify migration files** after they've been applied to any environment +- If you need to change something, **create a new migration file** instead +- Use version control to track migration history +- Consider using `spring.flyway.validate-on-migrate=true` (already enabled) to catch these issues early + ## System Context Diagram ```mermaid flowchart TD diff --git a/pom.xml b/pom.xml index a54dc101..3c908062 100644 --- a/pom.xml +++ b/pom.xml @@ -142,7 +142,7 @@ commons-io commons-io - 2.11.0 + 2.19.0 org.apache.poi diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java index 5cf91283..9c2a6dee 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ApplicationFormController.java @@ -215,7 +215,8 @@ public ResponseEntity updateApplicationForm(HttpServletRequest this.applicationFormService.patchApplicationForm(applicationId, applicationFormPatchDTO, false); - EventType eventType = applicationFormPatchDTO.getApplicationStatus().equals(ApplicationStatusEnum.PUBLISHED) + EventType eventType = applicationFormPatchDTO.getApplicationStatus() != null + && applicationFormPatchDTO.getApplicationStatus().equals(ApplicationStatusEnum.PUBLISHED) ? EventType.APPLICATION_PUBLISHED : EventType.APPLICATION_UPDATED; logApplicationEvent(eventType, request.getRequestedSessionId(), applicationId.toString()); diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ControllerExceptionHandler.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ControllerExceptionHandler.java index 9953aae1..982ae096 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ControllerExceptionHandler.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/ControllerExceptionHandler.java @@ -127,7 +127,7 @@ protected ResponseEntity handleConflict(ObjectOptimisticLockingFailureEx /** * This overridden method handles controller methods being called with invalid * arguments It'll spit back a json representation of a FieldErrorsDTO or a - * ClassErrorsDTO + * ClassErrorsDTO. */ @Override protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, @@ -140,7 +140,7 @@ protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotV /** * This overridden method handles controller methods which cause bind exceptions It'll - * spit back a json representation of a FieldErrorsDTO or a ClassErrorsDTO + * spit back a json representation of a FieldErrorsDTO or a ClassErrorsDTO. */ @Override protected ResponseEntity handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/HealthController.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/HealthController.java index 6d017579..92ae0e3e 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/HealthController.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/HealthController.java @@ -16,4 +16,9 @@ public ResponseEntity getHealthCheck() { return ResponseEntity.ok("Service up"); } + @GetMapping("/check") + public ResponseEntity getHealthCheck2() { + return ResponseEntity.ok("Admin API Service up"); + } + } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SchemeEditorController.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SchemeEditorController.java index 8ce2d2b9..0b7a8fec 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SchemeEditorController.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/controllers/SchemeEditorController.java @@ -55,7 +55,7 @@ public ResponseEntity> getSchemeEditors(@PathVariable fin @PostMapping public ResponseEntity addEditorToScheme(@PathVariable final Integer schemeId, @RequestBody @Valid final SchemeEditorPostDTO newEditorDto, final HttpServletRequest request) { final String jwt = HelperUtils.getJwtFromCookies(request, userServiceConfig.getCookieName()); - schemeEditorService.addEditorToScheme(schemeId, newEditorDto.getEditorEmailAddress(), jwt); + schemeEditorService.addEditorToScheme(schemeId, newEditorDto.getEditorEmailAddress().toLowerCase(), jwt); return ResponseEntity.ok("Editor added to scheme successfully"); } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/application/ApplicationFormPatchDTO.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/application/ApplicationFormPatchDTO.java index 47fe5ab2..2353aab1 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/application/ApplicationFormPatchDTO.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/application/ApplicationFormPatchDTO.java @@ -1,20 +1,30 @@ package gov.cabinetoffice.gap.adminbackend.dtos.application; -import javax.validation.constraints.NotNull; - +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import gov.cabinetoffice.gap.adminbackend.enums.ApplicationStatusEnum; -import lombok.AllArgsConstructor; +import gov.cabinetoffice.gap.adminbackend.validation.AtLeastOneFieldNotNull; import lombok.Data; import lombok.NoArgsConstructor; @Data -@AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = false) +@AtLeastOneFieldNotNull public class ApplicationFormPatchDTO { - @NotNull private ApplicationStatusEnum applicationStatus; + private Boolean allowsMultipleSubmissions; + + public ApplicationFormPatchDTO(ApplicationStatusEnum applicationStatus) { + this.applicationStatus = applicationStatus; + } + + public ApplicationFormPatchDTO(ApplicationStatusEnum applicationStatus, Boolean allowsMultipleSubmissions) { + this.applicationStatus = applicationStatus; + this.allowsMultipleSubmissions = allowsMultipleSubmissions; + } + } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/grantExport/ExportedSubmissionsDto.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/grantExport/ExportedSubmissionsDto.java index 77c5316e..9f082540 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/grantExport/ExportedSubmissionsDto.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/grantExport/ExportedSubmissionsDto.java @@ -18,4 +18,5 @@ public class ExportedSubmissionsDto { private UUID submissionId; private GrantExportStatus status; private ZonedDateTime submittedDate; + private String submissionName; } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/submission/LambdaSubmissionDefinition.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/submission/LambdaSubmissionDefinition.java index ce83a65b..977fbb65 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/submission/LambdaSubmissionDefinition.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/dtos/submission/LambdaSubmissionDefinition.java @@ -18,6 +18,8 @@ public class LambdaSubmissionDefinition { private String gapId; + private String submissionName; + private ZonedDateTime submittedDate; private List sections; diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/ApplicationFormEntity.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/ApplicationFormEntity.java index 0f18f745..7b98e953 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/ApplicationFormEntity.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/ApplicationFormEntity.java @@ -54,6 +54,9 @@ public class ApplicationFormEntity extends BaseEntity { @Enumerated(EnumType.STRING) private ApplicationStatusEnum applicationStatus; + @Column(name = "allows_multiple_submissions") + private Boolean allowsMultipleSubmissions; + @Column(name = "definition", nullable = false, columnDefinition = "json") @Type(type = "json") private ApplicationDefinitionDTO definition; @@ -70,6 +73,7 @@ public ApplicationFormEntity(Integer grantSchemeId, String applicationName, Inte this.lastUpdateBy = lastUpdateBy; this.lastPublished = null; this.applicationName = applicationName; + this.allowsMultipleSubmissions = false; this.definition = definition; } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/Submission.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/Submission.java index 63bc6987..b60c5035 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/Submission.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/entities/Submission.java @@ -77,6 +77,9 @@ public class Submission extends BaseEntity { @Column private String applicationName; + @Column(name = "submission_name") + private String submissionName; + @Column @Enumerated(EnumType.STRING) private SubmissionStatus status; diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/ApplicationFormMapper.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/ApplicationFormMapper.java index 876f25bb..412dc282 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/ApplicationFormMapper.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/ApplicationFormMapper.java @@ -6,9 +6,11 @@ import gov.cabinetoffice.gap.adminbackend.dtos.application.questions.QuestionOptionsPatchDTO; import gov.cabinetoffice.gap.adminbackend.dtos.application.questions.QuestionOptionsPostDTO; import gov.cabinetoffice.gap.adminbackend.entities.ApplicationFormEntity; +import org.mapstruct.BeanMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; import java.util.ArrayList; import java.util.List; @@ -52,6 +54,7 @@ public interface ApplicationFormMapper { List applicationFormFoundViewToDTO(List foundView); + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) void updateApplicationEntityFromPatchDto(ApplicationFormPatchDTO patchDTO, @MappingTarget ApplicationFormEntity applicationFormEntity); diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImpl.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImpl.java index 526dd02b..d3d04e2f 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImpl.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImpl.java @@ -60,6 +60,7 @@ public ExportedSubmissionsDto grantExportEntityToExportedSubmissions(GrantExport exportedSubmissionsDto.status(grantExportEntity.getStatus()); exportedSubmissionsDto.name(mapExportedSubmissionName(grantExportEntity)); exportedSubmissionsDto.submittedDate(mapExportedSubmissionSubmittedDate(grantExportEntity)); + exportedSubmissionsDto.submissionName(mapExportedSubmissionSubmissionName(grantExportEntity)); return exportedSubmissionsDto.build(); } @@ -96,6 +97,20 @@ public ZonedDateTime mapExportedSubmissionSubmittedDate(GrantExportEntity grantE return submission.get().getSubmittedDate(); } + + @Override + public String mapExportedSubmissionSubmissionName(GrantExportEntity grantExportEntity) { + log.info("Getting submission name from grant export {} and submission {}", grantExportEntity.getId(), + grantExportEntity.getId().getSubmissionId()); + final UUID submissionId = grantExportEntity.getId().getSubmissionId(); + final Optional submission = submissionRepository.findById(submissionId); + if (submission.isEmpty()) { + log.error("Submission not found for id: {}", submissionId); + return null; + } + return submission.get().getSubmissionName(); + } + private UUID grantExportEntityIdSubmissionId(GrantExportEntity grantExportEntity) { if (grantExportEntity == null) { return null; diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/GrantExportMapper.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/GrantExportMapper.java index f0b0cf48..94dc4194 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/GrantExportMapper.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/mappers/GrantExportMapper.java @@ -21,6 +21,7 @@ public interface GrantExportMapper { @Mapping(target = "name", expression = "java(mapExportedSubmissionName(grantExportEntity))") @Mapping(target ="status", source = "status") @Mapping(target = "submittedDate", expression = "java(mapExportedSubmissionSubmittedDate(grantExportEntity))") + @Mapping(target = "submissionName", expression = "java(mapExportedSubmissionSubmissionName(grantExportEntity))") ExportedSubmissionsDto grantExportEntityToExportedSubmissions(GrantExportEntity grantExportEntity); default String mapExportedSubmissionName(GrantExportEntity grantExportEntity) { @@ -31,4 +32,8 @@ default ZonedDateTime mapExportedSubmissionSubmittedDate(GrantExportEntity grant return null; } + default String mapExportedSubmissionSubmissionName(GrantExportEntity grantExportEntity) { + return ""; + } + } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/SpotlightBatchRepository.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/SpotlightBatchRepository.java index 78e3f877..a3573a32 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/SpotlightBatchRepository.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/repositories/SpotlightBatchRepository.java @@ -28,8 +28,8 @@ Optional findByStatusAndSpotlightSubmissionsSizeLessThan( Optional> findByStatus(@Param("status") SpotlightBatchStatus status); - @Query("select s from SpotlightBatch s inner join s.spotlightSubmissions spotlightSubmissions where s.status =:status and spotlightSubmissions.mandatoryQuestions.gapId = :gapId") - Optional findByStatusAndSpotlightSubmissions_MandatoryQuestions_GapId( + @Query("select s from SpotlightBatch s inner join s.spotlightSubmissions spotlightSubmissions where s.status =:status and spotlightSubmissions.mandatoryQuestions.gapId = :gapId ORDER BY s.created DESC") + List findByStatusAndSpotlightSubmissions_MandatoryQuestions_GapId( @Param("status") SpotlightBatchStatus status, @Param("gapId") String gapId); } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/security/WebSecurityConfig.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/security/WebSecurityConfig.java index fd570b74..4f182a29 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/security/WebSecurityConfig.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/security/WebSecurityConfig.java @@ -31,34 +31,45 @@ public WebSecurityConfig(final UserService userService, final JwtTokenFilterConf @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - //if you add a path which is hit by the lambda, remember to update also the paths in gov/cabinetoffice/gap/adminbackend/config/LambdasInterceptor.java + // if you add a path which is hit by the lambda, remember to update also the + // paths in gov/cabinetoffice/gap/adminbackend/config/LambdasInterceptor.java http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and() .authorizeHttpRequests(auth -> auth .mvcMatchers("/login", "/health", + "/health/check", "/emails/sendLambdaConfirmationEmail", "/users/validateAdminSession", "/submissions/{submissionId:" + UUID_REGEX_STRING - + "}/export-batch/{batchExportId:" + UUID_REGEX_STRING + "}/submission", + + "}/export-batch/{batchExportId:" + + UUID_REGEX_STRING + "}/submission", "/submissions/*/export-batch/*/status", - "/submissions/{submissionId:" + UUID_REGEX_STRING + "}/export-batch/{batchExportId:" + "/submissions/{submissionId:" + UUID_REGEX_STRING + + "}/export-batch/{batchExportId:" + UUID_REGEX_STRING + "}/s3-object-key", - "/grant-export/{exportId:" + UUID_REGEX_STRING + "}/outstandingCount", - "/grant-export/{exportId:" + UUID_REGEX_STRING + "}/failedCount", - "/grant-export/{exportId:" + UUID_REGEX_STRING + "}/remainingCount", - "/grant-export/{exportId:" + UUID_REGEX_STRING + "}/completed", - "/grant-export/{exportId:" + UUID_REGEX_STRING + "}/batch/status", - "/grant-export/{exportId:" + UUID_REGEX_STRING + "}/batch/s3-object-key", - "/grant-advert/lambda/{grantAdvertId:" + UUID_REGEX_STRING + "}/publish", - "/grant-advert/lambda/{grantAdvertId:" + UUID_REGEX_STRING + "}/unpublish", + "/grant-export/{exportId:" + UUID_REGEX_STRING + + "}/outstandingCount", + "/grant-export/{exportId:" + UUID_REGEX_STRING + + "}/failedCount", + "/grant-export/{exportId:" + UUID_REGEX_STRING + + "}/remainingCount", + "/grant-export/{exportId:" + UUID_REGEX_STRING + + "}/completed", + "/grant-export/{exportId:" + UUID_REGEX_STRING + + "}/batch/status", + "/grant-export/{exportId:" + UUID_REGEX_STRING + + "}/batch/s3-object-key", + "/grant-advert/lambda/{grantAdvertId:" + + UUID_REGEX_STRING + "}/publish", + "/grant-advert/lambda/{grantAdvertId:" + + UUID_REGEX_STRING + "}/unpublish", "/users/migrate", "/users/delete", "/users/tech-support-user/**", "/users/admin-user/**", "/users/funding-organisation", "/application-forms/lambda/**", - "/feedback/add" - ) + "/feedback/add") .permitAll() .antMatchers("/v3/api-docs/**", "/swagger-ui/**", @@ -66,7 +77,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/swagger-ui.html", "/webjars/**") .permitAll() - .antMatchers("/spotlight-submissions/{spotlightSubmissionId:" + UUID_REGEX_STRING + "}") + .antMatchers("/spotlight-submissions/{spotlightSubmissionId:" + + UUID_REGEX_STRING + "}") .permitAll() .antMatchers("/spotlight-batch/status/**", "/spotlight-batch", @@ -77,7 +89,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .anyRequest() .authenticated()) - .formLogin().disable().httpBasic().disable().logout().disable().csrf().disable().exceptionHandling() + .formLogin().disable().httpBasic().disable().logout().disable().csrf().disable() + .exceptionHandling() .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); http.addFilterAfter(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java index 4beddada..2ef65faa 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/ApplicationFormService.java @@ -336,14 +336,14 @@ public void patchApplicationForm(Integer applicationId, ApplicationFormPatchDTO ApplicationFormEntity application = this.applicationFormRepository.findById(applicationId) .orElseThrow(() -> new NotFoundException("Application with id " + applicationId + " does not exist.")); - if (patchDTO.getApplicationStatus() == ApplicationStatusEnum.PUBLISHED && application.getDefinition() + if (patchDTO.getApplicationStatus() != null && patchDTO.getApplicationStatus() == ApplicationStatusEnum.PUBLISHED && application.getDefinition() .getSections().stream().anyMatch(section -> section.getQuestions().isEmpty())) { throw new FieldViolationException("sections", "Cannot publish a form with a section that has no questions"); } try { this.applicationFormMapper.updateApplicationEntityFromPatchDto(patchDTO, application); - if (patchDTO.getApplicationStatus().equals(ApplicationStatusEnum.PUBLISHED)) { + if (patchDTO.getApplicationStatus() != null && patchDTO.getApplicationStatus().equals(ApplicationStatusEnum.PUBLISHED)) { application.setLastPublished(Instant.now()); } diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchService.java index 0be0c7f0..a5144302 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchService.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchService.java @@ -44,9 +44,12 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import static gov.cabinetoffice.gap.adminbackend.enums.DraftAssessmentResponseDtoStatus.SUCCESS; @@ -107,6 +110,18 @@ public SpotlightBatch addSpotlightSubmissionToSpotlightBatch(SpotlightSubmission UUID spotlightBatchId) { final SpotlightBatch spotlightBatch = getSpotlightBatch(spotlightBatchId); final List existingSpotlightSubmissions = spotlightBatch.getSpotlightSubmissions(); + + // Check if submission is already in the batch (idempotent operation) + boolean alreadyExists = existingSpotlightSubmissions != null + && existingSpotlightSubmissions.stream() + .anyMatch(s -> s.getId().equals(spotlightSubmission.getId())); + + if (alreadyExists) { + log.info("Submission {} is already in batch {}. Returning existing batch (idempotent).", + spotlightSubmission.getId(), spotlightBatchId); + return spotlightBatch; + } + final List existingSpotlightBatches = spotlightSubmission.getBatches(); existingSpotlightSubmissions.add(spotlightSubmission); @@ -124,11 +139,21 @@ private SpotlightBatch getSpotlightBatch(UUID spotlightBatchId) { } public SpotlightBatch getSpotlightBatchWithQueuedStatusByMandatoryQuestionGapId(String gapId) { - return spotlightBatchRepository - .findByStatusAndSpotlightSubmissions_MandatoryQuestions_GapId(SpotlightBatchStatus.QUEUED, gapId) - .orElseThrow(() -> new NotFoundException( - "A spotlight batch with spotlightSubmission for mandatory question with gap id " + gapId - + " could not be found")); + List batches = spotlightBatchRepository + .findByStatusAndSpotlightSubmissions_MandatoryQuestions_GapId(SpotlightBatchStatus.QUEUED, gapId); + + if (batches.isEmpty()) { + throw new NotFoundException( + "A spotlight batch with spotlightSubmission for mandatory question with gap id " + gapId + + " could not be found"); + } + + if (batches.size() > 1) { + log.warn("Found {} QUEUED batches for gapId {}. Using the most recent one.", batches.size(), gapId); + } + + // Return the first batch (already ordered by created DESC in the query) + return batches.get(0); } public List getSpotlightBatchesByStatus(SpotlightBatchStatus status) { @@ -140,10 +165,39 @@ public List generateSendToSpotlightDtosList(SpotlightBatchSt final List sendToSpotlightDtos = new ArrayList<>(); final List spotlightBatches = getSpotlightBatchesByStatus(status); + // Track submissions we've already included to prevent duplicates across batches + final Set processedSubmissionIds = new HashSet<>(); + for (SpotlightBatch spotlightBatch : spotlightBatches) { try { + final List submissions = spotlightBatch.getSpotlightSubmissions(); + + // Filter: Only non-SENT submissions that haven't been processed yet + final List submissionsToProcess = submissions.stream() + .filter(sub -> !SpotlightSubmissionStatus.SENT.toString().equals(sub.getStatus())) + .filter(sub -> !processedSubmissionIds.contains(sub.getId())) + .collect(Collectors.toList()); + + if (submissionsToProcess.isEmpty()) { + log.info("Batch {} skipped - no eligible submissions (all are SENT or already processed)", + spotlightBatch.getId()); + // Mark batch as SUCCESS since all submissions are already SENT + if (submissions.stream().allMatch(s -> SpotlightSubmissionStatus.SENT.toString().equals(s.getStatus()))) { + spotlightBatch.setStatus(SpotlightBatchStatus.SUCCESS); + spotlightBatch.setLastUpdated(Instant.now()); + spotlightBatchRepository.save(spotlightBatch); + } + continue; + } + + // Mark these submissions as processed to prevent duplicates in other batches + submissionsToProcess.forEach(sub -> processedSubmissionIds.add(sub.getId())); + + log.info("Processing batch {} with {} submissions (filtered from {})", + spotlightBatch.getId(), submissionsToProcess.size(), submissions.size()); + final List schemes = new ArrayList<>(); - addSpotlightSchemeDtoToList(spotlightBatch, schemes); + addSpotlightSchemeDtoToListFiltered(spotlightBatch, schemes, submissionsToProcess); final SendToSpotlightDto sendToSpotlightDto = SendToSpotlightDto.builder().schemes(schemes).build(); sendToSpotlightDtos.add(sendToSpotlightDto); @@ -157,6 +211,21 @@ public List generateSendToSpotlightDtosList(SpotlightBatchSt return sendToSpotlightDtos; } + /** + * Adds spotlight scheme DTOs to the list using a pre-filtered list of submissions. + * This prevents duplicate submissions being sent to Spotlight. + */ + private void addSpotlightSchemeDtoToListFiltered(SpotlightBatch spotlightBatch, + List schemes, List submissionsToProcess) { + + final List uniqueSchemeIds = getUniqueSchemeIds(submissionsToProcess); + log.info("uniqueSchemeIds for batch {}: {}", spotlightBatch.getId(), uniqueSchemeIds); + + uniqueSchemeIds.stream() + .map(uniqueSchemeId -> generateSchemeDto(uniqueSchemeId, submissionsToProcess)) + .forEach(schemes::add); + } + public List getUniqueSchemeIds(List spotlightSubmissions) { return spotlightSubmissions.stream() .map(submission -> submission.getMandatoryQuestions().getSchemeEntity().getGgisIdentifier()).distinct() @@ -282,16 +351,21 @@ private void handleError(SpotlightSubmission spotlightSubmission, private void handleErrorMessage(SpotlightSubmission spotlightSubmission, String errorMessage) { if (errorMessage.contains(RESPONSE_MESSAGE_406_SCHEME_NOT_EXIST)) { spotlightSubmission.setStatus(SpotlightSubmissionStatus.GGIS_ERROR.toString()); - sendMessageToQueue(spotlightSubmission); + // DON'T re-queue GGIS errors - they require manual intervention to fix the scheme ID + // Re-queuing these creates an infinite loop of failures + log.warn("GGIS_ERROR for submission {} - scheme doesn't exist in Spotlight. " + + "Manual intervention required to fix the GGIS identifier.", spotlightSubmission.getId()); } else if (errorMessage.contains(RESPONSE_MESSAGE_409_FIELD_MISSING) || errorMessage.contains(RESPONSE_MESSAGE_409_LENGTH)) { - // has a validation error + // has a validation error - DON'T re-queue as these need manual data fixes spotlightSubmission.setStatus(SpotlightSubmissionStatus.VALIDATION_ERROR.toString()); log.info("Sending Spotlight validation support email using SNS for status code: 409"); final String snsResponse = snsService.spotlightValidationError(); log.info(snsResponse); + log.warn("VALIDATION_ERROR for submission {} - {}. Manual intervention required.", + spotlightSubmission.getId(), errorMessage); } } @@ -315,8 +389,11 @@ public SpotlightResponseResultsDto sendBatchToSpotlight(SendToSpotlightDto spotl final HttpEntity requestEntity = new HttpEntity<>(spotlightBatchAsJsonString, requestHeaders); - final String draftAssessmentsEndpoint = spotlightConfig.getSpotlightUrl() - + "/services/apexrest/DraftAssessments"; + // Construct the endpoint URL - avoid duplicating the path if already in config + String baseUrl = spotlightConfig.getSpotlightUrl(); + final String draftAssessmentsEndpoint = baseUrl.contains("/services/apexrest/DraftAssessments") + ? baseUrl + : baseUrl + "/services/apexrest/DraftAssessments"; SpotlightResponseResultsDto list = SpotlightResponseResultsDto.builder().build(); diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/UserService.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/UserService.java index e3bc182d..e0bcd4d4 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/services/UserService.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/services/UserService.java @@ -116,8 +116,10 @@ public Boolean verifyAdminRoles(final String emailAddress, final String roles) { @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") public GrantAdmin getGrantAdminIdFromUserServiceEmail(final String email, final String jwt) { try { + //UI might send camelcase. change it to lowercase + UserV2DTO response = webClientBuilder.build().get() - .uri(userServiceConfig.getDomain() + "/user/email/" + email) + .uri(userServiceConfig.getDomain() + "/user/email/" + email.toLowerCase()) .cookie(userServiceConfig.getCookieName(), jwt).retrieve().bodyToMono(UserV2DTO.class).block(); return grantAdminRepository.findByGapUserUserSub(response.sub()).orElseThrow(() -> new NotFoundException( diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/AtLeastOneFieldNotNull.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/AtLeastOneFieldNotNull.java new file mode 100644 index 00000000..d2e9977c --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/AtLeastOneFieldNotNull.java @@ -0,0 +1,20 @@ +package gov.cabinetoffice.gap.adminbackend.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = AtLeastOneFieldNotNullValidator.class) +@Documented +public @interface AtLeastOneFieldNotNull { + String message() default "At least one field must be provided"; + Class[] groups() default {}; + Class[] payload() default {}; +} + diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/AtLeastOneFieldNotNullValidator.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/AtLeastOneFieldNotNullValidator.java new file mode 100644 index 00000000..91ae700e --- /dev/null +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/AtLeastOneFieldNotNullValidator.java @@ -0,0 +1,25 @@ +package gov.cabinetoffice.gap.adminbackend.validation; + +import gov.cabinetoffice.gap.adminbackend.dtos.application.ApplicationFormPatchDTO; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class AtLeastOneFieldNotNullValidator implements ConstraintValidator { + + @Override + public void initialize(AtLeastOneFieldNotNull constraintAnnotation) { + // No initialization needed + } + + @Override + public boolean isValid(ApplicationFormPatchDTO dto, ConstraintValidatorContext context) { + if (dto == null) { + return false; + } + + // Check if at least one field is not null + return dto.getApplicationStatus() != null || dto.getAllowsMultipleSubmissions() != null; + } +} + diff --git a/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidator.java b/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidator.java index 642588e6..e6da6264 100644 --- a/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidator.java +++ b/src/main/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidator.java @@ -216,7 +216,7 @@ private SimpleEntry validateURL(GrantAdvertQuestionResponse ques // (incl. .com) (repeating) | // (optional) slash | (optional) additional path | (optional) slash | // (optional) query params - String urlValidPattern = "^(http://|https://)(www.)?((?!www)[a-zA-Z0-9\\-]{2,}+)(\\.[a-zA-Z0-9\\-]{2,})+(/)?(/[a-z0-9\\-._~%!$&'()*+,;=:@]+)*?(/)?(\\?[a-z0-9\\-._~%!$&'()*+,;=:@/]*)?$"; + String urlValidPattern = "^(http://|https://)(www.)?((?!www)[a-zA-Z0-9\\-]{2,}+)(\\.[a-zA-Z0-9\\-]{2,})+(/)?(/[a-z0-9\\-._~%!$&'()*+,;=:@#]+)*?(/)?(\\?[a-z0-9\\-._~%!$&'()*+,;=:@#/]*)?$"; Pattern pattern = Pattern.compile(urlValidPattern, Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(question.getResponse()); diff --git a/src/main/resources/db/migration/V1_102__add_submission_name_to_grant_submission.sql b/src/main/resources/db/migration/V1_102__add_submission_name_to_grant_submission.sql new file mode 100644 index 00000000..b3386662 --- /dev/null +++ b/src/main/resources/db/migration/V1_102__add_submission_name_to_grant_submission.sql @@ -0,0 +1,2 @@ +ALTER TABLE grant_submission +ADD COLUMN IF NOT EXISTS submission_name VARCHAR(255); diff --git a/src/main/resources/db/migration/V1_103__add_allows_multiple_submissions_to_grant_application.sql b/src/main/resources/db/migration/V1_103__add_allows_multiple_submissions_to_grant_application.sql new file mode 100644 index 00000000..1f467199 --- /dev/null +++ b/src/main/resources/db/migration/V1_103__add_allows_multiple_submissions_to_grant_application.sql @@ -0,0 +1,3 @@ +ALTER TABLE grant_application +ADD COLUMN IF NOT EXISTS allows_multiple_submissions BOOLEAN DEFAULT false; + diff --git a/src/main/resources/db/migration/V1_76__adding_event-service_tables.sql b/src/main/resources/db/migration/V1_76__adding_event-service_tables.sql index 623f019a..d07ba354 100644 --- a/src/main/resources/db/migration/V1_76__adding_event-service_tables.sql +++ b/src/main/resources/db/migration/V1_76__adding_event-service_tables.sql @@ -1,6 +1,5 @@ -- !!!!! ENSURE event_stream schema has been created !!!! --- CREATE schema IF NOT EXISTS event_stream; - +CREATE schema IF NOT EXISTS event_stream; CREATE TABLE IF NOT EXISTS event_stream.event_log ( diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImplTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImplTest.java index 35ca3080..c5f5670e 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImplTest.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/mappers/CustomGrantExportMapperImplTest.java @@ -53,7 +53,8 @@ class CustomGrantExportMapperImplTest { .zipFileLocation("location") .status(GrantExportStatus.COMPLETE) .name("Some company name") - .submittedDate(FIXED_DATE_TIME) + .submittedDate(FIXED_DATE_TIME) + .submissionName("My Application Name") .build(); final SubmissionQuestion ORG_NAME_SUBMISSION_QUESTION = SubmissionQuestion.builder() .questionId("APPLICANT_ORG_NAME") @@ -81,7 +82,8 @@ class CustomGrantExportMapperImplTest { .status(SubmissionStatus.SUBMITTED) .createdBy(GrantApplicant.builder().id(1).userId(UUID.randomUUID().toString()).build()) .definition(submissionDefinition) - .submittedDate(FIXED_DATE_TIME) + .submittedDate(FIXED_DATE_TIME) + .submissionName("My Application Name") .build(); @Mock @@ -122,6 +124,34 @@ void grantExportEntityToExportedSubmissions_hasNoSubmission() { exportedSubmissionsDto.setName(submissionId.toString()); exportedSubmissionsDto.setSubmittedDate(null); + exportedSubmissionsDto.setSubmissionName(null); assertEquals(exportedSubmissionsDto, exportedSubmissions); } + + @Test + void grantExportEntityToExportedSubmissions_mapsSubmissionName() { + when(submissionRepository.findById(submissionId)).thenReturn(Optional.of(submission)); + + final ExportedSubmissionsDto exportedSubmissions = customGrantExportMapper.grantExportEntityToExportedSubmissions(grantExport); + + assertEquals("My Application Name", exportedSubmissions.getSubmissionName()); + } + + @Test + void grantExportEntityToExportedSubmissions_handlesNullSubmissionName() { + final Submission submissionWithoutName = Submission.builder() + .id(submissionId) + .scheme(scheme) + .status(SubmissionStatus.SUBMITTED) + .createdBy(GrantApplicant.builder().id(1).userId(UUID.randomUUID().toString()).build()) + .definition(submissionDefinition) + .submittedDate(FIXED_DATE_TIME) + .submissionName(null) + .build(); + when(submissionRepository.findById(submissionId)).thenReturn(Optional.of(submissionWithoutName)); + + final ExportedSubmissionsDto exportedSubmissions = customGrantExportMapper.grantExportEntityToExportedSubmissions(grantExport); + + assertEquals(null, exportedSubmissions.getSubmissionName()); + } } \ No newline at end of file diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantExportServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantExportServiceTest.java index e338ddb1..55e31be1 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantExportServiceTest.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/GrantExportServiceTest.java @@ -216,6 +216,7 @@ void generateExportedSubmissionsListDto() { .status(GrantExportStatus.COMPLETE) .name("name2") .submittedDate(date) + .submissionName("Application Name 2") .build(); final ExportedSubmissionsDto exportedSubmissionsDto2 = ExportedSubmissionsDto.builder() .submissionId(submissionId2) @@ -223,6 +224,7 @@ void generateExportedSubmissionsListDto() { .status(GrantExportStatus.COMPLETE) .name("name1") .submittedDate(oldDate) + .submissionName("Application Name 1") .build(); when(exportRepository.findByCreatedByAndId_ExportBatchIdAndStatus(SEC_CONTEXT_ADMIN_ID,mockExportId, GrantExportStatus.COMPLETE, pagination)) diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchServiceTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchServiceTest.java index e0fa2e7b..42fb5b6a 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchServiceTest.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/services/SpotlightBatchServiceTest.java @@ -520,7 +520,7 @@ void getSpotlightBatchByMandatoryQuestionGapId_success() { final SpotlightBatch spotlightBatch = SpotlightBatch.builder().id(uuid).build(); when(spotlightBatchRepository.findByStatusAndSpotlightSubmissions_MandatoryQuestions_GapId(any(), any())) - .thenReturn(Optional.of(spotlightBatch)); + .thenReturn(List.of(spotlightBatch)); final SpotlightBatch result = spotlightBatchService .getSpotlightBatchWithQueuedStatusByMandatoryQuestionGapId("GAP123"); @@ -531,7 +531,7 @@ void getSpotlightBatchByMandatoryQuestionGapId_success() { @Test void getSpotlightBatchByMandatoryQuestionGapId_notFound() { when(spotlightBatchRepository.findByStatusAndSpotlightSubmissions_MandatoryQuestions_GapId(any(), any())) - .thenReturn(Optional.empty()); + .thenReturn(List.of()); assertThrows(NotFoundException.class, () -> spotlightBatchService.getSpotlightBatchWithQueuedStatusByMandatoryQuestionGapId("GAP123")); @@ -722,7 +722,6 @@ void spotlightResponsesResultIsNotNullAndDraftAssessmentStatusIsFailureAndMessag doNothing().when(spotlightBatchService).updateSpotlightBatchStatus(sendToSpotlightDto, SpotlightBatchStatus.FAILURE); - doNothing().when(spotlightBatchService).sendMessageToQueue(spotlightSubmission); spotlightBatchService.processSpotlightResponse(sendToSpotlightDto, spotlightResponseResults); @@ -733,7 +732,8 @@ void spotlightResponsesResultIsNotNullAndDraftAssessmentStatusIsFailureAndMessag verify(spotlightBatchService, times(1)).updateSpotlightBatchStatus(sendToSpotlightDto, SpotlightBatchStatus.FAILURE); - verify(spotlightBatchService, times(1)).sendMessageToQueue(spotlightSubmission); + // GGIS_ERROR submissions should NOT be re-queued - they require manual intervention + verify(spotlightBatchService, never()).sendMessageToQueue(any()); } @Test diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/testdata/ApplicationFormTestData.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/testdata/ApplicationFormTestData.java index 979450b1..a612f176 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/testdata/ApplicationFormTestData.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/testdata/ApplicationFormTestData.java @@ -159,43 +159,43 @@ public static ApplicationFormQuestionDTO buildQuestion(ResponseTypeEnum respons public final static ApplicationFormEntity SAMPLE_APPLICATION_FORM_ENTITY = new ApplicationFormEntity( SAMPLE_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION); public final static ApplicationFormEntity SAMPLE_EMPTY_APPLICATION_FORM_ENTITY = new ApplicationFormEntity( SAMPLE_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_EMPTY_APPLICATION_DEFINITION); + ApplicationStatusEnum.DRAFT, false, SAMPLE_EMPTY_APPLICATION_DEFINITION); public final static ApplicationFormEntity SAMPLE_APPLICATION_FORM_ENTITY_DELETE_SECTION = new ApplicationFormEntity( SAMPLE_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION_DELETE_SECTION); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION_DELETE_SECTION); public final static ApplicationFormEntity SAMPLE_APPLICATION_FORM_ENTITY_DELETE_QUESTION = new ApplicationFormEntity( SAMPLE_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION_DELETE_QUESTION); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION_DELETE_QUESTION); public final static ApplicationFormEntity SAMPLE_APPLICATION_FORM_ENTITY_NO_QUESTIONS = new ApplicationFormEntity( SAMPLE_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION_NO_QUESTIONS); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION_NO_QUESTIONS); public final static ApplicationFormEntity SAMPLE_APPLICATION_FORM_ENTITY_SECTIONS_NO_QUESTIONS = new ApplicationFormEntity( SAMPLE_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION_SECTIONS_NO_QUESTIONS); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION_SECTIONS_NO_QUESTIONS); public final static ApplicationFormEntity SAMPLE_SECOND_APPLICATION_FORM_ENTITY = new ApplicationFormEntity( SAMPLE_SECOND_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION_WITH_OPTIONS_QUESTION); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION_WITH_OPTIONS_QUESTION); public final static ApplicationFormEntity SAMPLE_APPLICATION_FORM_ENTITY_WITH_DUPLICATE_SECTIONS = new ApplicationFormEntity( SAMPLE_SECOND_APPLICATION_ID, SAMPLE_SCHEME_ID, SAMPLE_VERSION, SAMPLE_CREATED, SAMPLE_CREATED_BY, SAMPLE_LAST_UPDATED, SAMPLE_LAST_UPDATE_BY, SAMPLE_LAST_PUBLISHED, SAMPLE_APPLICATION_NAME, - ApplicationStatusEnum.DRAFT, SAMPLE_APPLICATION_DEFINITION_WITH_DUPLICATE_SECTIONS); + ApplicationStatusEnum.DRAFT, false, SAMPLE_APPLICATION_DEFINITION_WITH_DUPLICATE_SECTIONS); public final static ApplicationFormExistsDTO SAMPLE_APPLICATION_FORM_EXISTS_DTO_SINGLE_PROP = new ApplicationFormExistsDTO( null, SAMPLE_SCHEME_ID, null); @@ -210,7 +210,7 @@ public static ApplicationFormQuestionDTO buildQuestion(ResponseTypeEnum respons SAMPLE_CLASS_ERROR_LIST_NO_PROPS_PROVIDED); public final static ApplicationFormEntity SAMPLE_ENTITY_EXAMPLE_SINGLE_PROP = new ApplicationFormEntity(null, - SAMPLE_SCHEME_ID, null, null, null, null, null, null, null, null, null); + SAMPLE_SCHEME_ID, null, null, null, null, null, null, null, null, null, null); public final static String SAMPLE_CLASS_ERROR_NO_PROPS_JSON_STRING = HelperUtils .asJsonString(SAMPLE_CLASS_ERROR_NO_PROPS_PROVIDED); diff --git a/src/test/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidatorTest.java b/src/test/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidatorTest.java index 3d5e622b..a525ddb4 100644 --- a/src/test/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidatorTest.java +++ b/src/test/java/gov/cabinetoffice/gap/adminbackend/validation/validators/AdvertPageResponseValidatorTest.java @@ -928,7 +928,8 @@ private static Stream validUrls() { Arguments.of("https://www.google.co.uk/long/path?query=var&query2=var"), Arguments.of("http://www.google.co.uk/long/path?query=var&query2=var"), Arguments.of("https://google.co.uk/long/path?query=var&query2=var"), - Arguments.of("http://google.co.uk/long/path?query=var&query2=var")); + Arguments.of("http://google.co.uk/long/path?query=var&query2=var"), + Arguments.of("https://google.co.uk/competition/1989/overview/63441f1e-850b-4581-986d-cd2f6c6c226d#summary")); } // @formatter:on