From f307207e16469a6fdf790b60047878986b276434 Mon Sep 17 00:00:00 2001 From: Russell Dodd Date: Fri, 9 Aug 2024 10:48:47 +0100 Subject: [PATCH] PO-591: GET Draft Account API implementation. --- build.gradle | 2 + ...DraftAccountControllerIntegrationTest.java | 39 +++++++--- .../controllers/DraftAccountController.java | 45 ++++++++--- .../opal/dto/GetDraftAccountResponseDto.java | 57 ++++++++++++++ .../uk/gov/hmcts/opal/dto/ToJsonString.java | 20 ++++- .../hmcts/opal/entity/DraftAccountEntity.java | 34 ++++----- .../JsonSchemaValidationException.java | 17 +++++ .../service/DraftAccountServiceInterface.java | 2 + .../opal/JsonSchemaValidationService.java | 70 ++++++++++++++++++ .../uk/gov/hmcts/opal/util/DateTimeUtils.java | 15 ++++ .../opal/util/KeepAsJsonDeserializer.java | 19 +++++ .../jsonSchemas/getDraftAccountResponse.json | 74 +++++++++++++++++++ .../DraftAccountControllerTest.java | 35 ++++++++- .../opal/JsonSchemaValidationServiceTest.java | 72 ++++++++++++++++++ .../resources/jsonSchemas/testSchema.json | 30 ++++++++ 15 files changed, 489 insertions(+), 42 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java create mode 100644 src/main/java/uk/gov/hmcts/opal/exception/JsonSchemaValidationException.java create mode 100644 src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java create mode 100644 src/main/java/uk/gov/hmcts/opal/util/DateTimeUtils.java create mode 100644 src/main/java/uk/gov/hmcts/opal/util/KeepAsJsonDeserializer.java create mode 100644 src/main/resources/jsonSchemas/getDraftAccountResponse.json create mode 100644 src/test/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationServiceTest.java create mode 100644 src/test/resources/jsonSchemas/testSchema.json diff --git a/build.gradle b/build.gradle index 0d2eebb69..2bec2d648 100644 --- a/build.gradle +++ b/build.gradle @@ -279,6 +279,8 @@ dependencies { implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '7.5.0' + implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.5.1'); + implementation group: 'io.rest-assured', name: 'rest-assured' implementation 'org.flywaydb:flyway-core' runtimeOnly 'org.flywaydb:flyway-database-postgresql:10.17.1' diff --git a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java index 46d39e626..8951d1901 100644 --- a/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerIntegrationTest.java @@ -5,19 +5,25 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import uk.gov.hmcts.opal.dto.ToJsonString; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; import uk.gov.hmcts.opal.service.opal.DraftAccountService; +import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; import java.time.LocalDate; +import java.util.logging.Logger; import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -31,6 +37,8 @@ @ActiveProfiles({"integration"}) class DraftAccountControllerIntegrationTest { + private static final Logger logger = Logger.getLogger(DraftAccountControllerIntegrationTest.class.getSimpleName()); + @Autowired MockMvc mockMvc; @@ -41,21 +49,31 @@ class DraftAccountControllerIntegrationTest { @MockBean UserStateService userStateService; + @SpyBean + private JsonSchemaValidationService jsonSchemaValidationService; + @Test void testGetDraftAccountById() throws Exception { DraftAccountEntity draftAccountEntity = createDraftAccountEntity(); when(draftAccountService.getDraftAccount(1L)).thenReturn(draftAccountEntity); - mockMvc.perform(get("/api/draft-account/1") + MvcResult result = mockMvc.perform(get("/api/draft-account/1") .header("authorization", "Bearer some_value")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.draftAccountId").value(1)) - .andExpect(jsonPath("$.businessUnit.businessUnitId").value(7)) - .andExpect(jsonPath("$.accountType").value("DRAFT")) - .andExpect(jsonPath("$.createdBy").value("Tony")) - .andExpect(jsonPath("$.accountStatus").value("CREATED")); + .andExpect(jsonPath("$.draft_account_id").value(1)) + .andExpect(jsonPath("$.business_unit_id").value(7)) + .andExpect(jsonPath("$.account_type").value("DRAFT")) + .andExpect(jsonPath("$.submitted_by").value("Tony")) + .andExpect(jsonPath("$.account_status").value("CREATED")) + .andReturn(); + + String body = result.getResponse().getContentAsString(); + + logger.info(":testGetDraftAccountById: Response body:\n" + ToJsonString.toPrettyJson(body)); + + assertTrue(jsonSchemaValidationService.isValid(body, "getDraftAccountResponse.json")); } @@ -83,7 +101,7 @@ void testPostDraftAccountsSearch() throws Exception { .andExpect(jsonPath("$[0].draftAccountId").value(1)) .andExpect(jsonPath("$[0].businessUnit.businessUnitId").value(7)) .andExpect(jsonPath("$[0].accountType").value("DRAFT")) - .andExpect(jsonPath("$[0].createdBy").value("Tony")) + .andExpect(jsonPath("$[0].submittedBy").value("Tony")) .andExpect(jsonPath("$[0].accountStatus").value("CREATED")); } @@ -100,10 +118,13 @@ private DraftAccountEntity createDraftAccountEntity() { return DraftAccountEntity.builder() .draftAccountId(1L) .businessUnit(BusinessUnitEntity.builder().businessUnitId((short)007).build()) - .createdDate(LocalDate.of(2023, 1, 2)) - .createdBy("Tony") + .createdDate(LocalDate.of(2023, 1, 2).atStartOfDay()) + .submittedBy("Tony") .accountType("DRAFT") .accountStatus("CREATED") + .account("{}") + .accountSnapshot("{}") + .timelineData("{}") .build(); } } diff --git a/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java b/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java index 5ac78ff1c..18a6f5160 100644 --- a/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java +++ b/src/main/java/uk/gov/hmcts/opal/controllers/DraftAccountController.java @@ -12,13 +12,17 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import uk.gov.hmcts.opal.dto.GetDraftAccountResponseDto; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; import uk.gov.hmcts.opal.entity.DraftAccountEntity; import uk.gov.hmcts.opal.service.opal.DraftAccountService; +import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; import java.util.List; +import java.util.Optional; +import static uk.gov.hmcts.opal.util.DateTimeUtils.toOffsetDateTime; import static uk.gov.hmcts.opal.util.HttpUtil.buildResponse; @@ -28,27 +32,32 @@ @Tag(name = "DraftAccount Controller") public class DraftAccountController { - private final DraftAccountService opalDraftAccountService; + private final DraftAccountService draftAccountService; private final UserStateService userStateService; - public DraftAccountController(UserStateService userStateService, DraftAccountService opalDraftAccountService) { - this.opalDraftAccountService = opalDraftAccountService; + private final JsonSchemaValidationService jsonSchemaValidationService; + + public DraftAccountController(UserStateService userStateService, DraftAccountService draftAccountService, + JsonSchemaValidationService jsonSchemaValidationService) { + this.draftAccountService = draftAccountService; this.userStateService = userStateService; + this.jsonSchemaValidationService = jsonSchemaValidationService; } @GetMapping(value = "/{draftAccountId}") @Operation(summary = "Returns the Draft Account for the given draftAccountId.") - public ResponseEntity getDraftAccountById(@PathVariable Long draftAccountId, - @RequestHeader(value = "Authorization", required = false) String authHeaderValue) { + public ResponseEntity getDraftAccountById( + @PathVariable Long draftAccountId, + @RequestHeader(value = "Authorization", required = false) String authHeaderValue) { log.info(":GET:getDraftAccountById: draftAccountId: {}", draftAccountId); userStateService.checkForAuthorisedUser(authHeaderValue); - DraftAccountEntity response = opalDraftAccountService.getDraftAccount(draftAccountId); + DraftAccountEntity response = draftAccountService.getDraftAccount(draftAccountId); - return buildResponse(response); + return buildResponse(Optional.ofNullable(response).map(this::toGetResponseDto).orElse(null)); } @PostMapping(value = "/search", consumes = MediaType.APPLICATION_JSON_VALUE) @@ -59,7 +68,7 @@ public ResponseEntity> postDraftAccountsSearch(@Request userStateService.checkForAuthorisedUser(authHeaderValue); - List response = opalDraftAccountService.searchDraftAccounts(criteria); + List response = draftAccountService.searchDraftAccounts(criteria); return buildResponse(response); } @@ -72,8 +81,26 @@ public ResponseEntity postDraftAccount(@RequestBody DraftAcc userStateService.checkForAuthorisedUser(authHeaderValue); - DraftAccountEntity response = opalDraftAccountService.saveDraftAccount(entity); + DraftAccountEntity response = draftAccountService.saveDraftAccount(entity); return buildResponse(response); } + + GetDraftAccountResponseDto toGetResponseDto(DraftAccountEntity entity) { + return GetDraftAccountResponseDto.builder() + .draftAccountId(entity.getDraftAccountId()) + .businessUnitId(entity.getBusinessUnit().getBusinessUnitId()) + .createdDate(toOffsetDateTime(entity.getCreatedDate())) + .submittedBy(entity.getSubmittedBy()) + .validatedDate(toOffsetDateTime(entity.getValidatedDate())) + .validatedBy(entity.getValidatedBy()) + .account(entity.getAccount()) + .accountSnapshot(entity.getAccountSnapshot()) + .accountType(entity.getAccountType()) + .accountStatus(entity.getAccountStatus()) + .timelineData(entity.getTimelineData()) + .accountNumber(entity.getAccountNumber()) + .accountId(entity.getAccountId()) + .build(); + } } diff --git a/src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java b/src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java new file mode 100644 index 000000000..10111f9c7 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/dto/GetDraftAccountResponseDto.java @@ -0,0 +1,57 @@ +package uk.gov.hmcts.opal.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GetDraftAccountResponseDto implements ToJsonString { + + @JsonProperty("draft_account_id") + private Long draftAccountId; + + @JsonProperty("business_unit_id") + private Short businessUnitId; + + @JsonProperty("created_at") + private OffsetDateTime createdDate; + + @JsonProperty("submitted_by") + private String submittedBy; + + @JsonProperty("validated_at") + private OffsetDateTime validatedDate; + + @JsonProperty("validated_by") + private String validatedBy; + + @JsonProperty("account") + private String account; + + @JsonProperty("account_snapshot") + private String accountSnapshot; + + @JsonProperty("account_type") + private String accountType; + + @JsonProperty("account_status") + private String accountStatus; + + @JsonProperty("timeline_data") + private String timelineData; + + @JsonProperty("account_number") + private String accountNumber; + + @JsonProperty("account_id") + private Long accountId; +} diff --git a/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java b/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java index d845992b1..050089f2e 100644 --- a/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java +++ b/src/main/java/uk/gov/hmcts/opal/dto/ToJsonString.java @@ -23,7 +23,11 @@ default String toJson() { } default String toPrettyJsonString() throws JsonProcessingException { - return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(this); + return toPrettyJsonString(this); + } + + static String toPrettyJsonString(Object original) throws JsonProcessingException { + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(original); } default String toPrettyJson() { @@ -34,8 +38,20 @@ default String toPrettyJson() { } } + static String toPrettyJson(String json) { + try { + return toPrettyJsonString(toJsonNode(json)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + default JsonNode toJsonNode() throws JsonProcessingException { - return OBJECT_MAPPER.readTree(this.toJsonString()); + return toJsonNode(this.toJsonString()); + } + + static JsonNode toJsonNode(String json) throws JsonProcessingException { + return OBJECT_MAPPER.readTree(json); } static ObjectMapper getObjectMapper() { diff --git a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java index 0db6e1f92..4f4a99404 100644 --- a/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java +++ b/src/main/java/uk/gov/hmcts/opal/entity/DraftAccountEntity.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonRawValue; import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -13,14 +14,13 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; -import jakarta.persistence.Temporal; -import jakarta.persistence.TemporalType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import uk.gov.hmcts.opal.util.KeepAsJsonDeserializer; -import java.time.LocalDate; +import java.time.LocalDateTime; @Entity @Table(name = "draft-accounts") @@ -44,28 +44,19 @@ public class DraftAccountEntity { private BusinessUnitEntity businessUnit; @Column(name = "created_date", nullable = false) - @Temporal(TemporalType.DATE) - private LocalDate createdDate; + private LocalDateTime createdDate; - @Column(name = "created_by", length = 20, nullable = false) - private String createdBy; - - @ManyToOne - @JoinColumn(name = "created_by_user_id", nullable = false) - private UserEntity createdByUser; + @Column(name = "submitted_by", length = 20, nullable = false) + private String submittedBy; @Column(name = "validated_date") - @Temporal(TemporalType.DATE) - private LocalDate validatedDate; + private LocalDateTime validatedDate; @Column(name = "validated_by", length = 20) private String validatedBy; - @ManyToOne - @JoinColumn(name = "validated_by_user_id") - private UserEntity validatedByUser; - @Column(name = "account", columnDefinition = "json", nullable = false) + @JsonDeserialize(using = KeepAsJsonDeserializer.class) @JsonRawValue private String account; @@ -75,14 +66,19 @@ public class DraftAccountEntity { @Column(name = "account_id") private Long accountId; - @Column(name = "account_summary_data", columnDefinition = "json", nullable = false) + @Column(name = "account_snapshot", columnDefinition = "json", nullable = false) + @JsonDeserialize(using = KeepAsJsonDeserializer.class) @JsonRawValue - private String accountSummaryData; + private String accountSnapshot; @Column(name = "account_status", length = 30, nullable = false) private String accountStatus; @Column(name = "status_reason", columnDefinition = "json") + @JsonDeserialize(using = KeepAsJsonDeserializer.class) @JsonRawValue private String timelineData; + + @Column(name = "account_number", length = 25) + private String accountNumber; } diff --git a/src/main/java/uk/gov/hmcts/opal/exception/JsonSchemaValidationException.java b/src/main/java/uk/gov/hmcts/opal/exception/JsonSchemaValidationException.java new file mode 100644 index 000000000..a8ff17807 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/exception/JsonSchemaValidationException.java @@ -0,0 +1,17 @@ +package uk.gov.hmcts.opal.exception; + +public class JsonSchemaValidationException extends RuntimeException { + + public JsonSchemaValidationException(String msg) { + super(msg); + } + + public JsonSchemaValidationException(Throwable t) { + super(t); + } + + public JsonSchemaValidationException(String message, Throwable t) { + super(message, t); + } + +} diff --git a/src/main/java/uk/gov/hmcts/opal/service/DraftAccountServiceInterface.java b/src/main/java/uk/gov/hmcts/opal/service/DraftAccountServiceInterface.java index d6556b783..7be5902c1 100644 --- a/src/main/java/uk/gov/hmcts/opal/service/DraftAccountServiceInterface.java +++ b/src/main/java/uk/gov/hmcts/opal/service/DraftAccountServiceInterface.java @@ -10,4 +10,6 @@ public interface DraftAccountServiceInterface { DraftAccountEntity getDraftAccount(long draftAccountId); List searchDraftAccounts(DraftAccountSearchDto criteria); + + } diff --git a/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java b/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java new file mode 100644 index 000000000..804e954be --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationService.java @@ -0,0 +1,70 @@ +package uk.gov.hmcts.opal.service.opal; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.util.StreamUtils; +import uk.gov.hmcts.opal.dto.ToJsonString; +import uk.gov.hmcts.opal.exception.JsonSchemaValidationException; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.Set; + +import static java.lang.String.format; + +@Slf4j(topic = "JsonSchemaValidationService") +@Service +public class JsonSchemaValidationService { + + private static final String PATH_ROOT = "jsonSchemas"; + + public boolean isValid(String body, String jsonSchemaFileName) { + Set errors = validate(body, jsonSchemaFileName); + if (!errors.isEmpty()) { + log.error(":isValid: for JSON schema '{}', found {} validation errors.", jsonSchemaFileName, errors.size()); + for (ValidationMessage msg : errors) { + log.error(":isValid: error: {}", msg.getMessage()); + } + return false; + } + return true; + } + + public Set validate(String body, String jsonSchemaFileName) { + String jsonSchemaContents = readJsonSchema(jsonSchemaFileName); + var jsonSchema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7).getSchema(jsonSchemaContents); + return jsonSchema.validate(getJsonNodeFromStringContent(body)); + } + + private JsonNode getJsonNodeFromStringContent(String content) { + try { + return ToJsonString.getObjectMapper().readTree(content); + } catch (JsonProcessingException e) { + throw new JsonSchemaValidationException(e.getMessage(), e); + } + } + + private String readJsonSchema(String schemaFileName) { + if (schemaFileName.isBlank()) { + throw new JsonSchemaValidationException("A schema filename is required to validate a JSON document."); + } + String filePath = Path.of(PATH_ROOT, schemaFileName).toString(); + ClassPathResource cpr = new ClassPathResource(filePath); + if (!cpr.exists()) { + throw new JsonSchemaValidationException(format("No JSON Schema file found at '%s'", cpr.getPath())); + } + try { + return StreamUtils.copyToString(cpr.getInputStream(), Charset.defaultCharset()); + } catch (IOException e) { + throw new JsonSchemaValidationException(format("Problem opening InputStream at '%s'", filePath), e); + } + } + +} diff --git a/src/main/java/uk/gov/hmcts/opal/util/DateTimeUtils.java b/src/main/java/uk/gov/hmcts/opal/util/DateTimeUtils.java new file mode 100644 index 000000000..1d205fc17 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/util/DateTimeUtils.java @@ -0,0 +1,15 @@ +package uk.gov.hmcts.opal.util; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Optional; + +public class DateTimeUtils { + + public static OffsetDateTime toOffsetDateTime(LocalDateTime localDateTime) { + return Optional.ofNullable(localDateTime) + .map(ldt -> ldt.atOffset(ZoneOffset.UTC)) + .orElse(null); + } +} diff --git a/src/main/java/uk/gov/hmcts/opal/util/KeepAsJsonDeserializer.java b/src/main/java/uk/gov/hmcts/opal/util/KeepAsJsonDeserializer.java new file mode 100644 index 000000000..f12b415da --- /dev/null +++ b/src/main/java/uk/gov/hmcts/opal/util/KeepAsJsonDeserializer.java @@ -0,0 +1,19 @@ +package uk.gov.hmcts.opal.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; + +public class KeepAsJsonDeserializer extends JsonDeserializer { + + @Override + public String deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + TreeNode tree = jp.getCodec().readTree(jp); + return tree.toString(); + } +} diff --git a/src/main/resources/jsonSchemas/getDraftAccountResponse.json b/src/main/resources/jsonSchemas/getDraftAccountResponse.json new file mode 100644 index 000000000..1adaac3ee --- /dev/null +++ b/src/main/resources/jsonSchemas/getDraftAccountResponse.json @@ -0,0 +1,74 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "draft_account_id": { + "type": "integer", + "format": "int64", + "description": "Unique ID for the Draft Account - System generated (BE)" + }, + "business_unit_id": { + "type": "integer", + "format": "int32", + "description": "ID of the Business Unit the Draft Account belongs to" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Date the Draft Account was created (first submitted) - System generated (BE)" + }, + "submitted_by": { + "type": "string", + "description": "ID of the User that last submitted the Draft Account for checking" + }, + "validated_at": { + "type": "string", + "format": "date-time", + "description": "[Optional] Date the Draft Account was validated - System generated (BE)" + }, + "validated_by": { + "type": "string", + "description": "[Optional] ID of the User that validated the Draft Account" + }, + "account": { + "type": "string", + "description": "The structured Account data (JSON)" + }, + "account_snapshot": { + "type": "string", + "description": "Summary business data to identify the Account - System generated (BE) from Account data (JSON)" + }, + "account_type": { + "type": "string", + "description": "Type of Account, such as Fixed Penalty Registration" + }, + "account_status": { + "type": "string", + "description": "Status of the Draft Account - one of Submitted, Resubmitted, Rejected, Approved, Deleted" + }, + "timeline_data": { + "type": "string", + "description": "Status changes to the Draft Account in chronological order (JSON Array) - System generated (UI)" + }, + "account_number": { + "type": "string", + "description": "[Optional] The Opal Account Number (2char letter code+account number) created on validation - System generated (BE)" + }, + "account_id": { + "type": "integer", + "format": "int64", + "description": "[Optional] Opal Account ID created on validation - System generated (BE)" + } + }, + "required": [ + "draft_account_id", + "business_unit_id", + "created_at", + "submitted_by", + "account", + "account_snapshot", + "account_type", + "account_status", + "timeline_data" + ] +} diff --git a/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java b/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java index cf2b5eecd..9fb95fd9b 100644 --- a/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java +++ b/src/test/java/uk/gov/hmcts/opal/controllers/DraftAccountControllerTest.java @@ -4,12 +4,16 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.opal.dto.GetDraftAccountResponseDto; import uk.gov.hmcts.opal.dto.search.DraftAccountSearchDto; +import uk.gov.hmcts.opal.entity.BusinessUnitEntity; import uk.gov.hmcts.opal.entity.DraftAccountEntity; import uk.gov.hmcts.opal.service.opal.DraftAccountService; +import uk.gov.hmcts.opal.service.opal.JsonSchemaValidationService; import uk.gov.hmcts.opal.service.opal.UserStateService; import java.util.List; @@ -19,6 +23,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static uk.gov.hmcts.opal.util.DateTimeUtils.toOffsetDateTime; @ExtendWith(MockitoExtension.class) class DraftAccountControllerTest { @@ -31,22 +36,28 @@ class DraftAccountControllerTest { @Mock private UserStateService userStateService; + @Spy + private JsonSchemaValidationService jsonSchemaValidationService; + @InjectMocks private DraftAccountController draftAccountController; @Test void testGetDraftAccount_Success() { // Arrange - DraftAccountEntity entity = DraftAccountEntity.builder().build(); + DraftAccountEntity entity = DraftAccountEntity.builder() + .businessUnit(BusinessUnitEntity.builder().build()) + .build(); when(draftAccountService.getDraftAccount(any(Long.class))).thenReturn(entity); // Act - ResponseEntity response = draftAccountController.getDraftAccountById(1L, BEARER_TOKEN); + ResponseEntity response = draftAccountController + .getDraftAccountById(1L, BEARER_TOKEN); // Assert assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(entity, response.getBody()); + assertEquals(toGetDto(entity), response.getBody()); verify(draftAccountService, times(1)).getDraftAccount(any(Long.class)); } @@ -85,4 +96,22 @@ void testSaveDraftAccounts_Success() { assertEquals(entity, response.getBody()); verify(draftAccountService, times(1)).saveDraftAccount(any()); } + + GetDraftAccountResponseDto toGetDto(DraftAccountEntity entity) { + return GetDraftAccountResponseDto.builder() + .draftAccountId(entity.getDraftAccountId()) + .businessUnitId(entity.getBusinessUnit().getBusinessUnitId()) + .createdDate(toOffsetDateTime(entity.getCreatedDate())) + .submittedBy(entity.getSubmittedBy()) + .validatedDate(toOffsetDateTime(entity.getValidatedDate())) + .validatedBy(entity.getValidatedBy()) + .account(entity.getAccount()) + .accountSnapshot(entity.getAccountSnapshot()) + .accountType(entity.getAccountType()) + .accountStatus(entity.getAccountStatus()) + .timelineData(entity.getTimelineData()) + .accountNumber(entity.getAccountNumber()) + .accountId(entity.getAccountId()) + .build(); + } } diff --git a/src/test/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationServiceTest.java b/src/test/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationServiceTest.java new file mode 100644 index 000000000..09c0f1d5c --- /dev/null +++ b/src/test/java/uk/gov/hmcts/opal/service/opal/JsonSchemaValidationServiceTest.java @@ -0,0 +1,72 @@ +package uk.gov.hmcts.opal.service.opal; + +import com.networknt.schema.ValidationMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.hmcts.opal.exception.JsonSchemaValidationException; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class JsonSchemaValidationServiceTest { + + @InjectMocks + private JsonSchemaValidationService jsonSchemaValidationService; + + @Test + void testIsValid_failBlankSchema() { + // Act + JsonSchemaValidationException jsve = assertThrows( + JsonSchemaValidationException.class, + () -> jsonSchemaValidationService.isValid("", " ") + ); + + // Assert + assertEquals("A schema filename is required to validate a JSON document.", + jsve.getMessage()); + } + + @Test + void testIsValid_failLoadSchema() { + // Act + JsonSchemaValidationException jsve = assertThrows( + JsonSchemaValidationException.class, + () -> jsonSchemaValidationService.isValid("", "nonExistentSchema.json") + ); + + // Assert + assertEquals("No JSON Schema file found at 'jsonSchemas/nonExistentSchema.json'", + jsve.getMessage()); + } + + @Test + void testIsValid_failIsValid() { + assertFalse(jsonSchemaValidationService.isValid("", "testSchema.json")); + } + + @Test + void testIsValid_failValidate1() { + Set messages = jsonSchemaValidationService + .validate("", "testSchema.json"); + assertEquals(1, messages.size()); + assertEquals("$: unknown found, object expected", messages + .stream() + .findFirst() + .map(ValidationMessage::getMessage) + .orElse("")); + } + + @Test + void testIsValid_failValidate2() { + Set messages = jsonSchemaValidationService + .validate("{\"data\": 7}", "testSchema.json"); + assertEquals(4, messages.size()); + } + +} diff --git a/src/test/resources/jsonSchemas/testSchema.json b/src/test/resources/jsonSchemas/testSchema.json new file mode 100644 index 000000000..9d80c049c --- /dev/null +++ b/src/test/resources/jsonSchemas/testSchema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "test_long_id": { + "type": "integer", + "format": "int64" + }, + "test_short_id": { + "type": "integer", + "format": "int32" + }, + "test_date_time": { + "type": "string", + "format": "date-time" + }, + "test_text_1": { + "type": "string" + }, + "test_text_2": { + "type": "string" + } + }, + "required": [ + "test_long_id", + "test_short_id", + "test_date_time", + "test_text_1" + ] +}