From 0a2bbfe651339b00dbc3f07676eb99284dc94f88 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 6 May 2026 15:48:46 +0100
Subject: [PATCH 01/18] Initial setup for csv creation and upload
---
.../data/api/controllers/CsvController.java | 28 +++
.../errorhandling/GlobalExceptionHandler.java | 9 +
.../exceptions/JsonConvertException.java | 7 +
.../data/api/models/StringMultipartFile.java | 61 ++++++
.../data/api/services/AzureBlobService.java | 22 ++
.../fact/data/api/services/CsvService.java | 34 ++++
.../fact/data/api/utils/CsvGenerator.java | 45 +++++
.../reform/fact/data/api/utils/CsvUtil.java | 190 ++++++++++++++++++
8 files changed, 396 insertions(+)
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/JsonConvertException.java
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvGenerator.java
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java
new file mode 100644
index 00000000..5aa1b19e
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java
@@ -0,0 +1,28 @@
+package uk.gov.hmcts.reform.fact.data.api.controllers;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import uk.gov.hmcts.reform.fact.data.api.security.SecuredFactRestController;
+import uk.gov.hmcts.reform.fact.data.api.services.CsvService;
+
+import java.util.List;
+
+@SecuredFactRestController(
+ name = "CSV",
+ description = "Operations related to CSV file handling"
+)
+@RequestMapping("/csv")
+public class CsvController {
+
+ private final CsvService csvService;
+
+ public CsvController(CsvService csvService) {
+ this.csvService = csvService;
+ }
+
+ @GetMapping("/files")
+ public ResponseEntity> getCsvFiles() {
+ return ResponseEntity.ok(csvService.getCsvFiles());
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java
index f336672d..15a2ace3 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java
@@ -2,12 +2,14 @@
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CourtResourceNotFoundException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.DuplicatedListItemException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidAreaOfLawException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidFileException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidPostcodeException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException;
+
import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID;
import java.time.LocalDateTime;
@@ -206,6 +208,13 @@ public ExceptionResponse handle(InvalidAreaOfLawException ex) {
return generateExceptionResponse(ex.getMessage());
}
+ @ExceptionHandler(JsonConvertException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ExceptionResponse handle(JsonConvertException ex) {
+ log.error("400, JSON conversion error. Details: {}", ex.getMessage());
+ return generateExceptionResponse(ex.getMessage());
+ }
+
private ExceptionResponse generateExceptionResponse(String message) {
ExceptionResponse response = new ExceptionResponse();
response.setMessage(message);
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/JsonConvertException.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/JsonConvertException.java
new file mode 100644
index 00000000..656c6bf1
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/JsonConvertException.java
@@ -0,0 +1,7 @@
+package uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions;
+
+import lombok.experimental.StandardException;
+
+@StandardException
+public class JsonConvertException extends RuntimeException {
+}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java
new file mode 100644
index 00000000..68ed38a9
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java
@@ -0,0 +1,61 @@
+package uk.gov.hmcts.reform.fact.data.api.models;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class StringMultipartFile implements MultipartFile {
+
+ private final String name;
+ private final String originalFileName;
+ private final String contentType;
+ private final byte[] content;
+
+ public StringMultipartFile(String name, String originalFileName, String contentType, String content) {
+ this.name = name;
+ this.originalFileName = originalFileName;
+ this.contentType = contentType;
+ this.content = content.getBytes();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getOriginalFilename() {
+ return originalFileName;
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return content.length == 0;
+ }
+
+ @Override
+ public long getSize() {
+ return content.length;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return content;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(content);
+ }
+
+ @Override
+ public void transferTo(java.io.File dest) throws IllegalStateException {
+ // Optionally, you can implement the logic to transfer the content to a file here.
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java
index 238fd82a..7cc0c221 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java
@@ -3,9 +3,11 @@
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.models.BlobHttpHeaders;
+import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
+import uk.gov.hmcts.reform.fact.data.api.models.StringMultipartFile;
import java.io.IOException;
@@ -53,4 +55,24 @@ public void deleteBlob(String imageId) {
blobClient.delete();
}
+
+
+ public void createCsvFileAndUpload(String containerName, String blobName, JsonNode jsonNodeData) {
+ try {
+ uploadFile(blobName,
+ createCsvFile(blobName, new CsvUtil().convertJsonToCsv(jsonNodeData))
+ );
+ } catch (IOException ex) {
+ throw new AzureUploadException(ex.getMessage());
+ }
+ }
+
+ public StringMultipartFile createCsvFile(String blobName, String csvString) {
+ return new StringMultipartFile(
+ blobName,
+ blobName,
+ "text/csv",
+ csvString
+ );
+ }
}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
new file mode 100644
index 00000000..2ed664e9
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
@@ -0,0 +1,34 @@
+package uk.gov.hmcts.reform.fact.data.api.services;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.stereotype.Service;
+
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
+
+/**
+ * Service for handling CSV file operations.
+ */
+@Service
+public class CsvService {
+
+ private final CourtService courtService;
+ private final ObjectMapper objectMapper;
+
+ public CsvService(CourtService courtService, ObjectMapper objectMapper) {
+ this.courtService = courtService;
+ this.objectMapper = objectMapper;
+ }
+
+ public String[] getCsvFiles() {
+ return new String[] {"courts.csv"};
+ }
+
+ public JsonNode convertCourtDataToJson() {
+ try {
+ return objectMapper.valueToTree(courtService.getAllCourtDetails());
+ } catch (IllegalArgumentException ex) {
+ throw new JsonConvertException("Error converting court data to JSON", ex);
+ }
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvGenerator.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvGenerator.java
new file mode 100644
index 00000000..a4f4c54c
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvGenerator.java
@@ -0,0 +1,45 @@
+package uk.gov.hmcts.reform.fact.data.api.runners;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+import uk.gov.hmcts.reform.fact.data.api.services.AzureBlobService;
+import uk.gov.hmcts.reform.fact.data.api.services.CourtService;
+
+@Component
+@Slf4j
+public class CsvGenerator implements CommandLineRunner {
+
+ private final AzureBlobService azureService;
+ private final CourtService courtService;
+
+ public CsvGenerator(@Autowired AzureBlobService azureService,
+ @Autowired CourtService courtService) {
+ this.azureService = azureService;
+ this.courtService = courtService;
+ }
+
+ /**
+ * Executes automatically when the Spring Boot application starts.
+ * This method is invoked as part of the application's startup process due to
+ * the implementation of the {@link CommandLineRunner} interface. At present, it triggers
+ * the generation of court and tribunal data in CSV format and uploads it to
+ * Azure Blob Storage. The method logs the start and completion of this process.
+ *
+ * @param args Optional command-line arguments passed during application startup.
+ */
+ @Override
+ public void run(String... args) {
+ log.info("Running CSV generation");
+ createCsvAndUpload();
+ log.info("Finished running CSV generation");
+ System.exit(0);
+ }
+
+ public void createCsvAndUpload() {
+ JsonNode courtData = courtService.getCourtData();
+ azureService.createCsvFileAndUpload("csv", "courts-and-tribunals-data.csv", courtData);
+ }
+}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
new file mode 100644
index 00000000..9c8a0ef8
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
@@ -0,0 +1,190 @@
+package uk.gov.hmcts.reform.fact.data.api.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class for converting court-related JSON data into a flat CSV format.
+ *
+ * This class uses the Jackson CSV and JSON libraries to parse and flatten complex, nested JSON structures
+ * representing court details into a simplified CSV format. It includes specific logic to handle various fields
+ * such as court names, geolocation, types, addresses, and areas of law, transforming them into string
+ * representations suitable for CSV output
.
+ *
+ * Key functionality includes:
+ * - Flattening nested JSON nodes (like addresses and areas of law) into single string fields.
+ * - Building a consistent CSV schema for output.
+ * - Safely handling missing or null fields to ensure robust CSV generation
.
+ *
+ * Intended for use in services or tools that need to export structured court data from JSON APIs into a
+ * human-readable and spreadsheet-friendly CSV format
.
+ */
+public class CsvUtil {
+
+ private final CsvMapper csvMapper;
+
+ protected CsvUtil(CsvMapper csvMapper) {
+ this.csvMapper = csvMapper;
+ }
+
+ public CsvUtil() {
+ this.csvMapper = new CsvMapper();
+ this.csvMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ }
+
+ public String convertJsonToCsv(JsonNode jsonArrayNode) {
+ CsvSchema schema = buildCsvSchema();
+ List
.
*/
public class CsvUtil {
+ private static final String NAME = "name";
+ private static final String LAT = "lat";
+ private static final String LON = "lon";
+ private static final String SLUG = "slug";
+ private static final String TYPES = "types";
+ private static final String AREAS_OF_LAW = "areas_of_law";
+ private static final String ADDRESSES = "addresses";
+ private static final String COURT_ADDRESSES = "courtAddresses";
+ private static final String COURT_TYPES = "courtTypes";
+ private static final String DX_NUMBER = "dx_number";
+ private static final String NOT_AVAILABLE = "N/A";
+ private static final String PIPE_SEPARATOR = " | ";
+ private static final String NO_AREAS_OF_LAW_AVAILABLE = "No areas of law available";
+ private static final String NO_ADDRESS_AVAILABLE = "No address available";
+ private static final String AREAS_OF_LAW_LABEL = "Areas of Law";
+ private static final String COURTS_LABEL = "Courts";
private final CsvMapper csvMapper;
@@ -58,18 +74,18 @@ public String convertJsonToCsv(JsonNode jsonArrayNode) {
private CsvSchema buildCsvSchema() {
return CsvSchema.builder()
- .addColumn("name")
- .addColumn("lat")
- .addColumn("lon")
+ .addColumn(NAME)
+ .addColumn(LAT)
+ .addColumn(LON)
.addColumn("number")
.addColumn("cci_code")
.addColumn("magistrate_code")
- .addColumn("slug")
- .addColumn("types")
+ .addColumn(SLUG)
+ .addColumn(TYPES)
.addColumn("open")
- .addColumn("dx_number")
- .addColumn("areas_of_law")
- .addColumn("addresses")
+ .addColumn(DX_NUMBER)
+ .addColumn(AREAS_OF_LAW)
+ .addColumn(ADDRESSES)
.build()
.withHeader();
}
@@ -77,22 +93,22 @@ private CsvSchema buildCsvSchema() {
public Map flattenCourtNode(JsonNode node) {
Map flatMap = new LinkedHashMap<>();
JsonNode courtCode = getFirstArrayItem(node, "courtCodes");
- JsonNode primaryAddress = getFirstArrayItem(node, "courtAddresses");
+ JsonNode primaryAddress = getFirstArrayItem(node, COURT_ADDRESSES);
- flatMap.put("name", node.path("name").asText());
- flatMap.put("lat", readDecimal(node, primaryAddress, "lat"));
- flatMap.put("lon", readDecimal(node, primaryAddress, "lon"));
+ flatMap.put(NAME, node.path(NAME).asText());
+ flatMap.put(LAT, readDecimal(node, primaryAddress, LAT));
+ flatMap.put(LON, readDecimal(node, primaryAddress, LON));
flatMap.put("number", readInteger(courtCode, "crownCourtCode", "crown_court_code"));
flatMap.put("cci_code", readInteger(courtCode, "countyCourtCode", "cci_code"));
flatMap.put("magistrate_code", readInteger(courtCode, "magistrateCourtCode", "magistrate_code",
"magistrate_court_code"));
- flatMap.put("slug", node.path("slug").asText());
- flatMap.put("types", flattenTypes(node));
+ flatMap.put(SLUG, node.path(SLUG).asText());
+ flatMap.put(TYPES, flattenTypes(node));
flatMap.put("open", readBoolean(node, "open", "displayed", "openOnCath"));
- flatMap.put("dx_number", flattenDxCodes(node));
+ flatMap.put(DX_NUMBER, flattenDxCodes(node));
- flatMap.put("areas_of_law", flattenAreasOfLaw(node.path("courtAreasOfLaw"), node.path("areas_of_law")));
- flatMap.put("addresses", flattenAddresses(node.path("courtAddresses"), node.path("addresses")));
+ flatMap.put(AREAS_OF_LAW, flattenAreasOfLaw(node.path("courtAreasOfLaw"), node.path(AREAS_OF_LAW)));
+ flatMap.put(ADDRESSES, flattenAddresses(node.path(COURT_ADDRESSES), node.path(ADDRESSES)));
return flatMap;
}
@@ -100,7 +116,7 @@ public Map flattenCourtNode(JsonNode node) {
private String flattenAreasOfLaw(JsonNode... candidateNodes) {
JsonNode courtAreasOfLaw = getFirstArrayCandidate(candidateNodes);
if (courtAreasOfLaw == null || courtAreasOfLaw.isEmpty()) {
- return "No areas of law available";
+ return NO_AREAS_OF_LAW_AVAILABLE;
}
List areas = new ArrayList<>();
@@ -110,7 +126,7 @@ private String flattenAreasOfLaw(JsonNode... candidateNodes) {
for (JsonNode area : areaList) {
String areaDetails = String.format(
"Name: %s, External Link: %s, Description: %s, Display Name: %s, Display External Link: %s",
- safeText(area, "name"),
+ safeText(area, NAME),
safeText(area, "externalLink", "external_link"),
safeText(area, "externalLinkDesc", "external_link_desc"),
safeText(area, "displayName", "display_name"),
@@ -121,16 +137,16 @@ private String flattenAreasOfLaw(JsonNode... candidateNodes) {
}
}
- return areas.isEmpty() ? "No areas of law available" : String.join(" | ", areas);
+ return areas.isEmpty() ? NO_AREAS_OF_LAW_AVAILABLE : String.join(PIPE_SEPARATOR, areas);
}
private String flattenAddresses(JsonNode... candidateNodes) {
JsonNode addressesNode = getFirstArrayCandidate(candidateNodes);
if (addressesNode == null) {
- return "No address available";
+ return NO_ADDRESS_AVAILABLE;
}
if (!addressesNode.isArray() || addressesNode.isEmpty()) {
- return "No address available";
+ return NO_ADDRESS_AVAILABLE;
}
List addresses = new ArrayList<>();
@@ -150,7 +166,7 @@ private String flattenAddresses(JsonNode... candidateNodes) {
addresses.add(addressDetails);
}
- return String.join(" | ", addresses);
+ return String.join(PIPE_SEPARATOR, addresses);
}
private String flattenAddressLines(JsonNode... candidateNodes) {
@@ -182,33 +198,33 @@ private String flattenFieldsOfLaw(JsonNode... candidateNodes) {
JsonNode addressNode = candidateNodes.length > 0 ? candidateNodes[candidateNodes.length - 1] : null;
if (addressNode != null && addressNode.isObject()) {
List parts = new ArrayList<>();
- addNamesFromArray(parts, addressNode.path("areasOfLaw"), "Areas of Law");
- addNamesFromArray(parts, addressNode.path("courtTypes"), "Courts");
- return parts.isEmpty() ? "N/A" : String.join(", ", parts);
+ addNamesFromArray(parts, addressNode.path("areasOfLaw"), AREAS_OF_LAW_LABEL);
+ addNamesFromArray(parts, addressNode.path(COURT_TYPES), COURTS_LABEL);
+ return parts.isEmpty() ? NOT_AVAILABLE : String.join(", ", parts);
}
- return "N/A";
+ return NOT_AVAILABLE;
}
List parts = new ArrayList<>();
JsonNode areas = fieldsOfLawNode.path("areasOfLaw");
if (areas.isMissingNode()) {
- areas = fieldsOfLawNode.path("areas_of_law");
+ areas = fieldsOfLawNode.path(AREAS_OF_LAW);
}
if (areas.isArray() && !areas.isEmpty()) {
List names = new ArrayList<>();
- areas.forEach(a -> names.add(a.isTextual() ? a.asText() : safeText(a, "name")));
- parts.add("Areas of Law: " + String.join(" | ", names));
+ areas.forEach(a -> names.add(a.isTextual() ? a.asText() : safeText(a, NAME)));
+ parts.add(AREAS_OF_LAW_LABEL + ": " + String.join(PIPE_SEPARATOR, names));
}
JsonNode courts = fieldsOfLawNode.path("courts");
if (courts.isMissingNode()) {
- courts = fieldsOfLawNode.path("courtTypes");
+ courts = fieldsOfLawNode.path(COURT_TYPES);
}
if (courts.isArray() && !courts.isEmpty()) {
List names = new ArrayList<>();
- courts.forEach(c -> names.add(c.isTextual() ? c.asText() : safeText(c, "name")));
- parts.add("Courts: " + String.join(" | ", names));
+ courts.forEach(c -> names.add(c.isTextual() ? c.asText() : safeText(c, NAME)));
+ parts.add(COURTS_LABEL + ": " + String.join(PIPE_SEPARATOR, names));
}
return String.join(", ", parts);
@@ -219,35 +235,35 @@ private String flattenDxCodes(JsonNode node) {
if (dxCodes.isArray() && !dxCodes.isEmpty()) {
List values = new ArrayList<>();
for (JsonNode dxCode : dxCodes) {
- String value = safeText(dxCode, "dxCode", "dx_number");
- if (!"N/A".equals(value)) {
+ String value = safeText(dxCode, "dxCode", DX_NUMBER);
+ if (!NOT_AVAILABLE.equals(value)) {
values.add(value);
}
}
if (!values.isEmpty()) {
- return String.join(" | ", values);
+ return String.join(PIPE_SEPARATOR, values);
}
}
- return node.path("dx_number").asText();
+ return node.path(DX_NUMBER).asText();
}
private String flattenTypes(JsonNode node) {
- if (node.path("types").isArray()) {
- return stringifyArray(node.path("types"));
+ if (node.path(TYPES).isArray()) {
+ return stringifyArray(node.path(TYPES));
}
List types = new ArrayList<>();
- JsonNode addresses = node.path("courtAddresses");
+ JsonNode addresses = node.path(COURT_ADDRESSES);
if (addresses.isArray()) {
for (JsonNode address : addresses) {
- JsonNode courtTypes = address.path("courtTypes");
+ JsonNode courtTypes = address.path(COURT_TYPES);
if (courtTypes.isArray()) {
for (JsonNode courtType : courtTypes) {
if (courtType.isTextual()) {
types.add(courtType.asText());
} else {
- String name = safeText(courtType, "name");
- if (!"N/A".equals(name)) {
+ String name = safeText(courtType, NAME);
+ if (!NOT_AVAILABLE.equals(name)) {
types.add(name);
}
}
@@ -260,11 +276,11 @@ private String flattenTypes(JsonNode node) {
JsonNode counterServiceOpeningHours = node.path("courtCounterServiceOpeningHours");
if (counterServiceOpeningHours.isArray()) {
for (JsonNode openingHour : counterServiceOpeningHours) {
- JsonNode courtTypes = openingHour.path("courtTypes");
+ JsonNode courtTypes = openingHour.path(COURT_TYPES);
if (courtTypes.isArray()) {
for (JsonNode courtType : courtTypes) {
- String name = courtType.isTextual() ? courtType.asText() : safeText(courtType, "name");
- if (!"N/A".equals(name) && !types.contains(name)) {
+ String name = courtType.isTextual() ? courtType.asText() : safeText(courtType, NAME);
+ if (!NOT_AVAILABLE.equals(name) && !types.contains(name)) {
types.add(name);
}
}
@@ -273,7 +289,7 @@ private String flattenTypes(JsonNode node) {
}
}
- return String.join(" | ", types);
+ return String.join(PIPE_SEPARATOR, types);
}
private JsonNode getFirstArrayItem(JsonNode node, String field) {
@@ -345,12 +361,12 @@ private String stringifyArray(JsonNode arrayNode) {
}
List items = new ArrayList<>();
arrayNode.forEach(n -> items.add(n.asText()));
- return String.join(" | ", items);
+ return String.join(PIPE_SEPARATOR, items);
}
private String safeText(JsonNode node, String... fieldNames) {
if (node == null) {
- return "N/A";
+ return NOT_AVAILABLE;
}
for (String fieldName : fieldNames) {
JsonNode field = node.path(fieldName);
@@ -358,11 +374,11 @@ private String safeText(JsonNode node, String... fieldNames) {
return field.asText();
}
}
- return "N/A";
+ return NOT_AVAILABLE;
}
private void addIfPresent(List values, String value) {
- if (value != null && !value.isBlank() && !"N/A".equals(value)) {
+ if (value != null && !value.isBlank() && !NOT_AVAILABLE.equals(value)) {
values.add(value);
}
}
@@ -374,14 +390,14 @@ private void addNamesFromArray(List parts, JsonNode arrayNode, String la
if (item.isTextual()) {
names.add(item.asText());
} else {
- String name = safeText(item, "name");
- if (!"N/A".equals(name)) {
+ String name = safeText(item, NAME);
+ if (!NOT_AVAILABLE.equals(name)) {
names.add(name);
}
}
});
if (!names.isEmpty()) {
- parts.add(label + ": " + String.join(" | ", names));
+ parts.add(label + ": " + String.join(PIPE_SEPARATOR, names));
}
}
}
From b1266a16cab72c3d88e91738c4a67791e20a9591 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 11:25:43 +0100
Subject: [PATCH 08/18] Add test file
---
.../reform/fact/data/api/utils/CsvUtil.java | 12 ++--
.../api/models/StringMultipartFileTest.java | 71 +++++++++++++++++++
2 files changed, 78 insertions(+), 5 deletions(-)
create mode 100644 src/test/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFileTest.java
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
index 75d02f2d..8db01e26 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
@@ -34,6 +34,8 @@ public class CsvUtil {
private static final String LON = "lon";
private static final String SLUG = "slug";
private static final String TYPES = "types";
+ private static final String CCI_CODE = "cci_code";
+ private static final String MAGISTRATE_CODE = "magistrate_code";
private static final String AREAS_OF_LAW = "areas_of_law";
private static final String ADDRESSES = "addresses";
private static final String COURT_ADDRESSES = "courtAddresses";
@@ -78,8 +80,8 @@ private CsvSchema buildCsvSchema() {
.addColumn(LAT)
.addColumn(LON)
.addColumn("number")
- .addColumn("cci_code")
- .addColumn("magistrate_code")
+ .addColumn(CCI_CODE)
+ .addColumn(MAGISTRATE_CODE)
.addColumn(SLUG)
.addColumn(TYPES)
.addColumn("open")
@@ -99,9 +101,9 @@ public Map flattenCourtNode(JsonNode node) {
flatMap.put(LAT, readDecimal(node, primaryAddress, LAT));
flatMap.put(LON, readDecimal(node, primaryAddress, LON));
flatMap.put("number", readInteger(courtCode, "crownCourtCode", "crown_court_code"));
- flatMap.put("cci_code", readInteger(courtCode, "countyCourtCode", "cci_code"));
- flatMap.put("magistrate_code", readInteger(courtCode, "magistrateCourtCode", "magistrate_code",
- "magistrate_court_code"));
+ flatMap.put(CCI_CODE, readInteger(courtCode, "countyCourtCode", "county_court_code", CCI_CODE));
+ flatMap.put(MAGISTRATE_CODE, readInteger(
+ courtCode, "magistrateCourtCode", "magistrate_court_code", MAGISTRATE_CODE));
flatMap.put(SLUG, node.path(SLUG).asText());
flatMap.put(TYPES, flattenTypes(node));
flatMap.put("open", readBoolean(node, "open", "displayed", "openOnCath"));
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFileTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFileTest.java
new file mode 100644
index 00000000..cb0483d1
--- /dev/null
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFileTest.java
@@ -0,0 +1,71 @@
+package uk.gov.hmcts.reform.fact.data.api.models;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class StringMultipartFileTest {
+
+ private static final String NAME = "testFile";
+ private static final String ORIGINAL_FILE_NAME = "test.csv";
+ private static final String CONTENT_TYPE = "text/csv";
+ private static final String CONTENT = "This is test content.";
+
+ @Test
+ void shouldReturnCorrectName() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ assertThat(file.getName()).isEqualTo(NAME);
+ }
+
+ @Test
+ void shouldReturnCorrectOriginalFileName() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ assertThat(file.getOriginalFilename()).isEqualTo(ORIGINAL_FILE_NAME);
+ }
+
+ @Test
+ void shouldReturnCorrectContentType() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ assertThat(file.getContentType()).isEqualTo(CONTENT_TYPE);
+ }
+
+ @Test
+ void shouldNotBeEmptyWhenContentIsProvided() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ assertThat(file.isEmpty()).isFalse();
+ }
+
+ @Test
+ void shouldBeEmptyWhenContentIsEmpty() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, "");
+ assertThat(file.isEmpty()).isTrue();
+ }
+
+ @Test
+ void shouldReturnCorrectSize() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ assertThat(file.getSize()).isEqualTo(CONTENT.getBytes().length);
+ }
+
+ @Test
+ void shouldReturnCorrectBytes() {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ assertThat(file.getBytes()).isEqualTo(CONTENT.getBytes());
+ }
+
+ @Test
+ void shouldReturnCorrectInputStreamContent() throws IOException {
+ StringMultipartFile file = new StringMultipartFile(NAME, ORIGINAL_FILE_NAME, CONTENT_TYPE, CONTENT);
+ InputStream inputStream = file.getInputStream();
+
+ byte[] buffer = new byte[CONTENT.length()];
+ int bytesRead = inputStream.read(buffer);
+
+ assertThat(bytesRead).isEqualTo(CONTENT.length());
+ assertThat(new String(buffer)).isEqualTo(CONTENT);
+ }
+}
+
From 2ad3866714f33b09ede71d7bf506cbb14e878c32 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 12:05:52 +0100
Subject: [PATCH 09/18] fix sonar cloud
---
.../reform/fact/data/api/utils/CsvUtil.java | 82 +++++++------
.../fact/data/api/utils/CsvUtilTest.java | 108 ++++++++++++++++++
2 files changed, 156 insertions(+), 34 deletions(-)
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
index 8db01e26..935824bd 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
@@ -37,6 +37,7 @@ public class CsvUtil {
private static final String CCI_CODE = "cci_code";
private static final String MAGISTRATE_CODE = "magistrate_code";
private static final String AREAS_OF_LAW = "areas_of_law";
+ private static final String AREAS_OF_LAW_PATH = "areasOfLaw";
private static final String ADDRESSES = "addresses";
private static final String COURT_ADDRESSES = "courtAddresses";
private static final String COURT_TYPES = "courtTypes";
@@ -123,7 +124,7 @@ private String flattenAreasOfLaw(JsonNode... candidateNodes) {
List areas = new ArrayList<>();
for (JsonNode courtArea : courtAreasOfLaw) {
- JsonNode areaList = courtArea.path("areasOfLaw");
+ JsonNode areaList = courtArea.path(AREAS_OF_LAW_PATH);
if (areaList.isArray()) {
for (JsonNode area : areaList) {
String areaDetails = String.format(
@@ -197,19 +198,29 @@ private String flattenAddressLines(JsonNode... candidateNodes) {
private String flattenFieldsOfLaw(JsonNode... candidateNodes) {
JsonNode fieldsOfLawNode = getFirstObjectCandidate(candidateNodes);
if (fieldsOfLawNode == null || !fieldsOfLawNode.isObject()) {
- JsonNode addressNode = candidateNodes.length > 0 ? candidateNodes[candidateNodes.length - 1] : null;
- if (addressNode != null && addressNode.isObject()) {
- List parts = new ArrayList<>();
- addNamesFromArray(parts, addressNode.path("areasOfLaw"), AREAS_OF_LAW_LABEL);
- addNamesFromArray(parts, addressNode.path(COURT_TYPES), COURTS_LABEL);
- return parts.isEmpty() ? NOT_AVAILABLE : String.join(", ", parts);
- }
- return NOT_AVAILABLE;
+ return flattenFieldsOfLawFromAddress(candidateNodes);
}
List parts = new ArrayList<>();
+ addFieldsOfLawAreas(parts, fieldsOfLawNode);
+ addFieldsOfLawCourts(parts, fieldsOfLawNode);
+
+ return parts.isEmpty() ? NOT_AVAILABLE : String.join(", ", parts);
+ }
+
+ private String flattenFieldsOfLawFromAddress(JsonNode... candidateNodes) {
+ JsonNode addressNode = candidateNodes.length > 0 ? candidateNodes[candidateNodes.length - 1] : null;
+ if (addressNode != null && addressNode.isObject()) {
+ List parts = new ArrayList<>();
+ addNamesFromArray(parts, addressNode.path(AREAS_OF_LAW_PATH), AREAS_OF_LAW_LABEL);
+ addNamesFromArray(parts, addressNode.path(COURT_TYPES), COURTS_LABEL);
+ return parts.isEmpty() ? NOT_AVAILABLE : String.join(", ", parts);
+ }
+ return NOT_AVAILABLE;
+ }
- JsonNode areas = fieldsOfLawNode.path("areasOfLaw");
+ private void addFieldsOfLawAreas(List parts, JsonNode fieldsOfLawNode) {
+ JsonNode areas = fieldsOfLawNode.path(AREAS_OF_LAW_PATH);
if (areas.isMissingNode()) {
areas = fieldsOfLawNode.path(AREAS_OF_LAW);
}
@@ -218,7 +229,9 @@ private String flattenFieldsOfLaw(JsonNode... candidateNodes) {
areas.forEach(a -> names.add(a.isTextual() ? a.asText() : safeText(a, NAME)));
parts.add(AREAS_OF_LAW_LABEL + ": " + String.join(PIPE_SEPARATOR, names));
}
+ }
+ private void addFieldsOfLawCourts(List parts, JsonNode fieldsOfLawNode) {
JsonNode courts = fieldsOfLawNode.path("courts");
if (courts.isMissingNode()) {
courts = fieldsOfLawNode.path(COURT_TYPES);
@@ -228,8 +241,6 @@ private String flattenFieldsOfLaw(JsonNode... candidateNodes) {
courts.forEach(c -> names.add(c.isTextual() ? c.asText() : safeText(c, NAME)));
parts.add(COURTS_LABEL + ": " + String.join(PIPE_SEPARATOR, names));
}
-
- return String.join(", ", parts);
}
private String flattenDxCodes(JsonNode node) {
@@ -255,43 +266,46 @@ private String flattenTypes(JsonNode node) {
}
List types = new ArrayList<>();
- JsonNode addresses = node.path(COURT_ADDRESSES);
+ collectTypesFromAddresses(node.path(COURT_ADDRESSES), types);
+
+ if (types.isEmpty()) {
+ collectTypesFromCounterService(node.path("courtCounterServiceOpeningHours"), types);
+ }
+
+ return String.join(PIPE_SEPARATOR, types);
+ }
+
+ private void collectTypesFromAddresses(JsonNode addresses, List types) {
if (addresses.isArray()) {
for (JsonNode address : addresses) {
JsonNode courtTypes = address.path(COURT_TYPES);
if (courtTypes.isArray()) {
for (JsonNode courtType : courtTypes) {
- if (courtType.isTextual()) {
- types.add(courtType.asText());
- } else {
- String name = safeText(courtType, NAME);
- if (!NOT_AVAILABLE.equals(name)) {
- types.add(name);
- }
- }
+ addTypeNameToList(courtType, types);
}
}
}
}
+ }
- if (types.isEmpty()) {
- JsonNode counterServiceOpeningHours = node.path("courtCounterServiceOpeningHours");
- if (counterServiceOpeningHours.isArray()) {
- for (JsonNode openingHour : counterServiceOpeningHours) {
- JsonNode courtTypes = openingHour.path(COURT_TYPES);
- if (courtTypes.isArray()) {
- for (JsonNode courtType : courtTypes) {
- String name = courtType.isTextual() ? courtType.asText() : safeText(courtType, NAME);
- if (!NOT_AVAILABLE.equals(name) && !types.contains(name)) {
- types.add(name);
- }
- }
+ private void collectTypesFromCounterService(JsonNode counterServiceOpeningHours, List types) {
+ if (counterServiceOpeningHours.isArray()) {
+ for (JsonNode openingHour : counterServiceOpeningHours) {
+ JsonNode courtTypes = openingHour.path(COURT_TYPES);
+ if (courtTypes.isArray()) {
+ for (JsonNode courtType : courtTypes) {
+ addTypeNameToList(courtType, types);
}
}
}
}
+ }
- return String.join(PIPE_SEPARATOR, types);
+ private void addTypeNameToList(JsonNode courtType, List types) {
+ String name = courtType.isTextual() ? courtType.asText() : safeText(courtType, NAME);
+ if (!NOT_AVAILABLE.equals(name) && !types.contains(name)) {
+ types.add(name);
+ }
}
private JsonNode getFirstArrayItem(JsonNode node, String field) {
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
index 851a8086..c95fa5f4 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
@@ -88,4 +88,112 @@ void shouldHandleOpenOnCath() {
assertThat(result.get("open")).isEqualTo(true);
}
+
+ @Test
+ void shouldReturnNotAvailableIfNoNodesProvided() {
+ Map result = csvUtil.flattenCourtNode(mapper.createObjectNode());
+ assertThat(result.get("addresses").toString()).isEqualTo("No address available");
+ }
+
+ @Test
+ void shouldFlattenTypesFromAddresses() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode addresses = root.putArray("courtAddresses");
+ ObjectNode address = addresses.addObject();
+ ArrayNode courtTypes = address.putArray("courtTypes");
+ courtTypes.add("County Court");
+ ObjectNode type2 = courtTypes.addObject();
+ type2.put("name", "Tribunal");
+
+ Map result = csvUtil.flattenCourtNode(root);
+
+ assertThat(result.get("types").toString()).isEqualTo("County Court | Tribunal");
+ }
+
+ @Test
+ void shouldFlattenTypesFromCounterService() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode counterService = root.putArray("courtCounterServiceOpeningHours");
+ ObjectNode service = counterService.addObject();
+ ArrayNode courtTypes = service.putArray("courtTypes");
+ courtTypes.add("Magistrates' Court");
+
+ Map result = csvUtil.flattenCourtNode(root);
+
+ assertThat(result.get("types").toString()).isEqualTo("Magistrates' Court");
+ }
+
+ @Test
+ void shouldAvoidDuplicateTypesFromCounterService() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode counterService = root.putArray("courtCounterServiceOpeningHours");
+
+ ObjectNode service1 = counterService.addObject();
+ service1.putArray("courtTypes").add("Magistrates' Court");
+
+ ObjectNode service2 = counterService.addObject();
+ service2.putArray("courtTypes").add("Magistrates' Court");
+
+ Map result = csvUtil.flattenCourtNode(root);
+
+ assertThat(result.get("types").toString()).isEqualTo("Magistrates' Court");
+ }
+
+ @Test
+ void shouldFlattenFieldsOfLawWithAreasAndCourts() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode addresses = root.putArray("courtAddresses");
+ ObjectNode address = addresses.addObject();
+ ObjectNode fieldsOfLaw = address.putObject("fieldsOfLaw");
+
+ ArrayNode areas = fieldsOfLaw.putArray("areasOfLaw");
+ ObjectNode a1 = areas.addObject();
+ a1.put("name", "Family");
+
+ ArrayNode courts = fieldsOfLaw.putArray("courts");
+ ObjectNode c1 = courts.addObject();
+ c1.put("name", "High Court");
+
+ Map result = csvUtil.flattenCourtNode(root);
+ String addressesStr = result.get("addresses").toString();
+ assertThat(addressesStr).contains("Areas of Law: Family");
+ assertThat(addressesStr).contains("Courts: High Court");
+ }
+
+ @Test
+ void shouldFlattenFieldsOfLawWithTextualAreasAndCourts() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode addresses = root.putArray("courtAddresses");
+ ObjectNode address = addresses.addObject();
+ ObjectNode fieldsOfLaw = address.putObject("fieldsOfLaw");
+
+ ArrayNode areas = fieldsOfLaw.putArray("areasOfLaw");
+ areas.add("Family");
+
+ ArrayNode courts = fieldsOfLaw.putArray("courts");
+ courts.add("High Court");
+
+ Map result = csvUtil.flattenCourtNode(root);
+ String addressesStr = result.get("addresses").toString();
+ assertThat(addressesStr).contains("Areas of Law: Family");
+ assertThat(addressesStr).contains("Courts: High Court");
+ }
+
+ @Test
+ void shouldFallbackToAddressNodeWhenFieldsOfLawNodeMissing() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode addresses = root.putArray("courtAddresses");
+ ObjectNode address = addresses.addObject();
+
+ ArrayNode addressAreas = address.putArray("areasOfLaw");
+ addressAreas.add("Crime");
+
+ ArrayNode addressTypes = address.putArray("courtTypes");
+ addressTypes.add("Crown Court");
+
+ Map result = csvUtil.flattenCourtNode(root);
+ String addressesStr = result.get("addresses").toString();
+ assertThat(addressesStr).contains("Areas of Law: Crime");
+ assertThat(addressesStr).contains("Courts: Crown Court");
+ }
}
From 79a353391312793fee23d55c97a2e4bc6f060b6d Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 12:25:22 +0100
Subject: [PATCH 10/18] increase test coverage
---
.../GlobalExceptionHandlerTest.java | 22 ++--
.../fact/data/api/utils/CsvUtilTest.java | 107 ++++++++++++++++++
2 files changed, 121 insertions(+), 8 deletions(-)
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java
index 58d74474..0bb2cd9a 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java
@@ -15,14 +15,8 @@
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CourtResourceNotFoundException;
-
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.DuplicatedListItemException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidAreaOfLawException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidFileException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.*;
+
import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID;
import java.lang.annotation.Annotation;
@@ -327,6 +321,18 @@ void testHandleInvalidAreaOfLawTypeException() {
assertThat(response.getTimestamp()).isNotNull();
}
+ @Test
+ void testHandleJsonConvertExceptionException() {
+ JsonConvertException ex =
+ new JsonConvertException("Json Convert Exception Message");
+
+ ExceptionResponse response = handler.handle(ex);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getMessage()).contains("Json Convert Exception Message");
+ assertThat(response.getTimestamp()).isNotNull();
+ }
+
private ConstraintViolation> createConstraintViolation(
Annotation annotation,
Object invalidValue,
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
index c95fa5f4..6d1de4f2 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
@@ -1,13 +1,20 @@
package uk.gov.hmcts.reform.fact.data.api.utils;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
class CsvUtilTest {
@@ -196,4 +203,104 @@ void shouldFallbackToAddressNodeWhenFieldsOfLawNodeMissing() {
assertThat(addressesStr).contains("Areas of Law: Crime");
assertThat(addressesStr).contains("Courts: Crown Court");
}
+
+ @Test
+ void shouldConvertJsonToCsv() {
+ ArrayNode root = mapper.createArrayNode();
+ ObjectNode court = root.addObject();
+ court.put("name", "Test Court");
+ court.put("slug", "test-court");
+
+ String csv = csvUtil.convertJsonToCsv(root);
+
+ assertThat(csv).contains("name,lat,lon,number,cci_code,magistrate_code,slug,types,open,dx_number,areas_of_law,addresses");
+ assertThat(csv).contains("Test Court");
+ assertThat(csv).contains("test-court");
+ }
+
+ @Test
+ void shouldThrowExceptionOnInvalidJson() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode addresses = root.putArray("courtAddresses");
+ ObjectNode address = addresses.addObject();
+ address.put("addressLine1", "Line 1");
+ address.put("addressLine2", "Line 2");
+
+ Map result = csvUtil.flattenCourtNode(root);
+ assertThat(result.get("addresses").toString()).contains("Address: Line 1, Line 2");
+
+ ArrayNode lines = address.putArray("addressLines");
+ lines.add("Line A");
+ lines.add("Line B");
+
+ result = csvUtil.flattenCourtNode(root);
+ assertThat(result.get("addresses").toString()).contains("Address: Line A, Line B");
+ }
+
+ @Test
+ void shouldHandleDxCodes() {
+ ObjectNode root = mapper.createObjectNode();
+ root.put("dx_number", "DX 123");
+
+ Map result = csvUtil.flattenCourtNode(root);
+ assertThat(result.get("dx_number")).isEqualTo("DX 123");
+
+ ArrayNode dxCodes = root.putArray("courtDxCodes");
+ ObjectNode dx1 = dxCodes.addObject();
+ dx1.put("dxCode", "DX 456");
+
+ result = csvUtil.flattenCourtNode(root);
+ assertThat(result.get("dx_number")).isEqualTo("DX 456");
+ }
+
+ @Test
+ void shouldReadNumbersAndBooleansCorrectly() {
+ ObjectNode root = mapper.createObjectNode();
+ root.put("open", true);
+
+ ArrayNode courtCodes = root.putArray("courtCodes");
+ ObjectNode code = courtCodes.addObject();
+ code.put("crownCourtCode", 123);
+ code.put("countyCourtCode", 456);
+ code.put("magistrateCourtCode", 789);
+
+ ArrayNode addresses = root.putArray("courtAddresses");
+ ObjectNode address = addresses.addObject();
+ address.put("lat", 51.5);
+ address.put("lon", -0.1);
+
+ Map result = csvUtil.flattenCourtNode(root);
+ assertThat(result.get("open")).isEqualTo(true);
+ assertThat(result.get("number")).isEqualTo(123);
+ assertThat(result.get("cci_code")).isEqualTo(456);
+ assertThat(result.get("magistrate_code")).isEqualTo(789);
+ assertThat(result.get("lat")).isEqualTo(51.5);
+ assertThat(result.get("lon")).isEqualTo(-0.1);
+ }
+
+ @Test
+ void shouldHandleNullsInReadMethods() {
+ ObjectNode root = mapper.createObjectNode();
+ Map result = csvUtil.flattenCourtNode(root);
+
+ assertThat(result.get("lat")).isNull();
+ assertThat(result.get("number")).isNull();
+ assertThat(result.get("open")).isEqualTo(false);
+ }
+
+ @Test
+ void shouldThrowJsonConvertExceptionWhenCsvWritingFails() throws Exception {
+ CsvMapper mockCsvMapper = mock(CsvMapper.class);
+ CsvUtil utilWithMock = new CsvUtil(mockCsvMapper);
+
+ when(mockCsvMapper.writer(org.mockito.ArgumentMatchers.any(com.fasterxml.jackson.dataformat.csv.CsvSchema.class)))
+ .thenThrow(new RuntimeException("Mock failure"));
+
+ ArrayNode root = mapper.createArrayNode();
+ root.addObject();
+
+ assertThatThrownBy(() -> utilWithMock.convertJsonToCsv(root))
+ .isInstanceOf(JsonConvertException.class)
+ .hasMessageContaining("Failed to convert JSON to CSV: Mock failure");
+ }
}
From 8e43a4b81d149181e90f98dffb2373beaf0e226b Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 12:31:10 +0100
Subject: [PATCH 11/18] Fix build
---
.../api/errorhandling/GlobalExceptionHandlerTest.java | 9 ++++++++-
.../hmcts/reform/fact/data/api/utils/CsvUtilTest.java | 7 ++++---
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java
index 0bb2cd9a..6357876c 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandlerTest.java
@@ -15,7 +15,14 @@
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.*;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CourtResourceNotFoundException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.DuplicatedListItemException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidAreaOfLawException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidDateRangeException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidFileException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidParameterCombinationException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.NotFoundException;
import uk.gov.hmcts.reform.fact.data.api.validation.annotations.ValidUUID;
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
index 6d1de4f2..caff3f13 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
@@ -12,7 +12,6 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -213,7 +212,8 @@ void shouldConvertJsonToCsv() {
String csv = csvUtil.convertJsonToCsv(root);
- assertThat(csv).contains("name,lat,lon,number,cci_code,magistrate_code,slug,types,open,dx_number,areas_of_law,addresses");
+ assertThat(csv)
+ .contains("name,lat,lon,number,cci_code,magistrate_code,slug,types,open,dx_number,areas_of_law,addresses");
assertThat(csv).contains("Test Court");
assertThat(csv).contains("test-court");
}
@@ -293,7 +293,8 @@ void shouldThrowJsonConvertExceptionWhenCsvWritingFails() throws Exception {
CsvMapper mockCsvMapper = mock(CsvMapper.class);
CsvUtil utilWithMock = new CsvUtil(mockCsvMapper);
- when(mockCsvMapper.writer(org.mockito.ArgumentMatchers.any(com.fasterxml.jackson.dataformat.csv.CsvSchema.class)))
+ when(mockCsvMapper.writer(org.mockito.ArgumentMatchers.any(
+ com.fasterxml.jackson.dataformat.csv.CsvSchema.class)))
.thenThrow(new RuntimeException("Mock failure"));
ArrayNode root = mapper.createArrayNode();
From 1498f22d6cc8339048604034de4e0348e8630b1e Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 14:13:20 +0100
Subject: [PATCH 12/18] Specify exceptions
---
.../exceptions/CsvCreationException.java | 7 +++++++
.../reform/fact/data/api/services/CsvService.java | 6 ++++--
.../exceptions/CustomExceptionsTest.java | 12 ++++++++++++
.../fact/data/api/services/CsvServiceTest.java | 8 +++++---
4 files changed, 28 insertions(+), 5 deletions(-)
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CsvCreationException.java
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CsvCreationException.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CsvCreationException.java
new file mode 100644
index 00000000..741052ef
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CsvCreationException.java
@@ -0,0 +1,7 @@
+package uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions;
+
+import lombok.experimental.StandardException;
+
+@StandardException
+public class CsvCreationException extends RuntimeException {
+}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
index 5177b467..ce506573 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
@@ -6,6 +6,8 @@
import org.springframework.stereotype.Service;
import uk.gov.hmcts.reform.fact.data.api.clients.SlackClient;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CsvCreationException;
import uk.gov.hmcts.reform.fact.data.api.models.StringMultipartFile;
import uk.gov.hmcts.reform.fact.data.api.utils.CsvUtil;
@@ -63,7 +65,7 @@ public void uploadCsvToAzureBlob(List actions, StringMultipartFile strin
} catch (Exception e) {
log.error("Error while uploading CSV", e);
actions.add("Failed to upload CSV file to Azure Blob Storage. Check App insights.");
- throw new RuntimeException("Failed to upload CSV file to Azure Blob Storage", e);
+ throw new AzureUploadException("Failed to upload CSV file to Azure Blob Storage", e);
}
}
@@ -83,7 +85,7 @@ public StringMultipartFile createCsvFile(List actions) {
} catch (Exception e) {
log.error("Error while creating CSV file", e);
actions.add("Failed to create CSV file. Check App insights.");
- throw new RuntimeException("Failed to create CSV file", e);
+ throw new CsvCreationException("Failed to create CSV file", e);
}
}
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CustomExceptionsTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CustomExceptionsTest.java
index 528b7b9e..150f0efe 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CustomExceptionsTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/exceptions/CustomExceptionsTest.java
@@ -47,4 +47,16 @@ void testCreationOfInvalidAreaOfLawTypeException() {
InvalidAreaOfLawException exception = new InvalidAreaOfLawException(TEST_MESSAGE);
assertEquals(TEST_MESSAGE, exception.getMessage(), ASSERTION_MESSAGE);
}
+
+ @Test
+ void testCreationOfCsvCreationException() {
+ CsvCreationException exception = new CsvCreationException(TEST_MESSAGE);
+ assertEquals(TEST_MESSAGE, exception.getMessage(), ASSERTION_MESSAGE);
+ }
+
+ @Test
+ void testCreationOfAzureUploadException() {
+ AzureUploadException exception = new AzureUploadException(TEST_MESSAGE);
+ assertEquals(TEST_MESSAGE, exception.getMessage(), ASSERTION_MESSAGE);
+ }
}
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
index 853128b9..0a401cea 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
@@ -6,6 +6,8 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import uk.gov.hmcts.reform.fact.data.api.clients.SlackClient;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CsvCreationException;
import uk.gov.hmcts.reform.fact.data.api.models.StringMultipartFile;
import java.util.ArrayList;
@@ -63,7 +65,7 @@ void uploadCsvToAzureBlobShouldAppendActionAndThrowWhenUploadFails() {
.when(azureBlobService)
.uploadFile(CSV_CONTAINER_NAME, CSV_FILE_NAME, csvFile);
- RuntimeException exception = assertThrows(RuntimeException.class, () ->
+ AzureUploadException exception = assertThrows(AzureUploadException.class, () ->
csvService.uploadCsvToAzureBlob(actions, csvFile)
);
@@ -88,7 +90,7 @@ void createAndUploadCsvShouldSendSlackMessageAndThrowWhenCsvCreationFails() {
CsvService csvService = buildService();
when(courtService.getAllCourtDetails()).thenThrow(new RuntimeException("court failure"));
- RuntimeException exception = assertThrows(RuntimeException.class, csvService::createAndUploadCsv);
+ CsvCreationException exception = assertThrows(CsvCreationException.class, csvService::createAndUploadCsv);
assertThat(exception.getMessage()).isEqualTo("Failed to create CSV file");
verify(slackClient).sendSlackMessage(contains("Failed to create CSV file. Check App insights."));
@@ -103,7 +105,7 @@ void createAndUploadCsvShouldSendSlackMessageAndThrowWhenUploadFails() {
.uploadFile(org.mockito.ArgumentMatchers.eq(CSV_CONTAINER_NAME),
org.mockito.ArgumentMatchers.eq(CSV_FILE_NAME), any(StringMultipartFile.class));
- RuntimeException exception = assertThrows(RuntimeException.class, csvService::createAndUploadCsv);
+ AzureUploadException exception = assertThrows(AzureUploadException.class, csvService::createAndUploadCsv);
assertThat(exception.getMessage()).isEqualTo("Failed to upload CSV file to Azure Blob Storage");
verify(slackClient)
From 488029ee2543e9f033d1772571ea29ca33774da2 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 14:47:16 +0100
Subject: [PATCH 13/18] Update exception handling
---
.../api/controllers/CsvControllerTest.java | 28 ++++++++++++++-----
.../data/api/controllers/CsvController.java | 3 +-
.../errorhandling/GlobalExceptionHandler.java | 16 +++++++++++
3 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java
index 28b742a2..e713e53a 100644
--- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java
@@ -9,12 +9,14 @@
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CsvCreationException;
import uk.gov.hmcts.reform.fact.data.api.services.CsvService;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Feature("CSV Controller")
@@ -40,14 +42,26 @@ void createAndUploadCsvReturns200() throws Exception {
}
@Test
- @DisplayName("POST /csv/ propagates exception when CSV creation/upload fails")
- void createAndUploadCsvThrowsException() {
- doThrow(new RuntimeException("Failed to create or upload CSV file"))
+ @DisplayName("POST /csv/ returns 502 when Azure upload fails")
+ void createAndUploadCsvReturns502OnAzureUploadException() throws Exception {
+ doThrow(new AzureUploadException("Failed to upload CSV file to Azure Blob Storage"))
.when(csvService)
.createAndUploadCsv();
- assertThatThrownBy(() -> mvc.perform(post("/csv/")))
- .hasRootCauseInstanceOf(RuntimeException.class)
- .hasMessageContaining("Failed to create or upload CSV file");
+ mvc.perform(post("/csv/"))
+ .andExpect(status().isBadGateway())
+ .andExpect(jsonPath("$.message").value("Failed to upload CSV file to Azure Blob Storage"));
+ }
+
+ @Test
+ @DisplayName("POST /csv/ returns 500 when CSV creation fails")
+ void createAndUploadCsvReturns500OnCsvCreationException() throws Exception {
+ doThrow(new CsvCreationException("Failed to create CSV file"))
+ .when(csvService)
+ .createAndUploadCsv();
+
+ mvc.perform(post("/csv/"))
+ .andExpect(status().isInternalServerError())
+ .andExpect(jsonPath("$.message").value("Failed to create CSV file"));
}
}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java
index b24f7ac6..f9d606d5 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvController.java
@@ -28,7 +28,8 @@ public CsvController(CsvService csvService) {
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "CSV file created and uploaded successfully"),
- @ApiResponse(responseCode = "500", description = "Failed to create or upload CSV file")
+ @ApiResponse(responseCode = "500", description = "Failed to create CSV file"),
+ @ApiResponse(responseCode = "502", description = "Failed to upload CSV file to storage")
})
public void createAndUploadCsv() {
csvService.createAndUploadCsv();
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java
index 15a2ace3..31c1601f 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/errorhandling/GlobalExceptionHandler.java
@@ -1,6 +1,8 @@
package uk.gov.hmcts.reform.fact.data.api.errorhandling;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CourtResourceNotFoundException;
+import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CsvCreationException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.DuplicatedListItemException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.InvalidAreaOfLawException;
@@ -215,6 +217,20 @@ public ExceptionResponse handle(JsonConvertException ex) {
return generateExceptionResponse(ex.getMessage());
}
+ @ExceptionHandler(AzureUploadException.class)
+ @ResponseStatus(HttpStatus.BAD_GATEWAY)
+ public ExceptionResponse handle(AzureUploadException ex) {
+ log.error("502, error while uploading CSV to Azure. Details: {}", ex.getMessage());
+ return generateExceptionResponse(ex.getMessage());
+ }
+
+ @ExceptionHandler(CsvCreationException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public ExceptionResponse handle(CsvCreationException ex) {
+ log.error("500, error while creating CSV file. Details: {}", ex.getMessage());
+ return generateExceptionResponse(ex.getMessage());
+ }
+
private ExceptionResponse generateExceptionResponse(String message) {
ExceptionResponse response = new ExceptionResponse();
response.setMessage(message);
From de8c63f72f22b7d3b0ebc865dc1b4cd9309c5b90 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 13 May 2026 15:05:54 +0100
Subject: [PATCH 14/18] Update CsvUtilTest.java
---
.../fact/data/api/utils/CsvUtilTest.java | 23 ++++++++++---------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
index caff3f13..384c1b82 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
@@ -59,14 +59,15 @@ void shouldFlattenEnrichedAddressesAndFieldsOfLaw() {
Map result = csvUtil.flattenCourtNode(root);
String addressesStr = result.get("addresses").toString();
- assertThat(addressesStr).contains("Address: 10 High St");
- assertThat(addressesStr).contains("Town: London");
- assertThat(addressesStr).contains("Postcode: SW1 1AA");
- assertThat(addressesStr).contains("Areas of Law: Crime");
- assertThat(addressesStr).contains("Courts: Crown Court");
+ assertThat(addressesStr)
+ .contains("Address: 10 High St")
+ .contains("Town: London")
+ .contains("Postcode: SW1 1AA")
+ .contains("Areas of Law: Crime")
+ .contains("Courts: Crown Court");
// Verify types flattening as well
- assertThat(result.get("types").toString()).isEqualTo("Crown Court");
+ assertThat(result.get("types")).hasToString("Crown Court");
}
@Test
@@ -82,7 +83,7 @@ void shouldFlattenTypesFromCounterServiceIfAddressesEmpty() {
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("types").toString()).isEqualTo("Magistrates' Court");
+ assertThat(result.get("types")).hasToString("Magistrates' Court");
}
@Test
@@ -98,7 +99,7 @@ void shouldHandleOpenOnCath() {
@Test
void shouldReturnNotAvailableIfNoNodesProvided() {
Map result = csvUtil.flattenCourtNode(mapper.createObjectNode());
- assertThat(result.get("addresses").toString()).isEqualTo("No address available");
+ assertThat(result.get("addresses")).hasToString("No address available");
}
@Test
@@ -113,7 +114,7 @@ void shouldFlattenTypesFromAddresses() {
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("types").toString()).isEqualTo("County Court | Tribunal");
+ assertThat(result.get("types")).hasToString("County Court | Tribunal");
}
@Test
@@ -126,7 +127,7 @@ void shouldFlattenTypesFromCounterService() {
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("types").toString()).isEqualTo("Magistrates' Court");
+ assertThat(result.get("types")).hasToString("Magistrates' Court");
}
@Test
@@ -142,7 +143,7 @@ void shouldAvoidDuplicateTypesFromCounterService() {
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("types").toString()).isEqualTo("Magistrates' Court");
+ assertThat(result.get("types")).hasToString("Magistrates' Court");
}
@Test
From 3d3f94a2d72f1a6c1ad0a2471e38432e69c4436b Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Fri, 15 May 2026 16:21:06 +0100
Subject: [PATCH 15/18] Address comments on PR
---
infrastructure/storage-account.tf | 4 ++
...ureBlobConfigurationTestConfiguration.java | 26 ++++++++-
.../api/controllers/CsvControllerTest.java | 43 +++++++++-----
.../api/config/AzureBlobConfiguration.java | 26 ++++++++-
.../data/api/models/StringMultipartFile.java | 58 +++++++++----------
.../data/api/services/AzureBlobService.java | 52 +++++------------
.../data/api/services/CourtPhotoService.java | 5 +-
.../fact/data/api/services/CsvService.java | 12 ++--
src/main/resources/application.yaml | 3 +-
.../config/AzureBlobConfigurationTest.java | 15 ++++-
.../api/services/AzureBlobServiceTest.java | 33 +++++------
.../api/services/CourtPhotoServiceTest.java | 14 +++--
.../data/api/services/CsvServiceTest.java | 15 +++--
13 files changed, 178 insertions(+), 128 deletions(-)
diff --git a/infrastructure/storage-account.tf b/infrastructure/storage-account.tf
index 036d0e7d..b3192ca1 100644
--- a/infrastructure/storage-account.tf
+++ b/infrastructure/storage-account.tf
@@ -16,6 +16,10 @@ module "storage_account" {
{
name = "photos",
access_type = "container"
+ },
+ {
+ name = "csv",
+ access_type = "container"
}
]
diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTestConfiguration.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTestConfiguration.java
index 95eec118..6f74f929 100644
--- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTestConfiguration.java
+++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTestConfiguration.java
@@ -9,6 +9,8 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
+import uk.gov.hmcts.reform.fact.data.api.services.AzureBlobService;
+
@Configuration
@Profile("test")
public class AzureBlobConfigurationTestConfiguration {
@@ -17,7 +19,10 @@ public class AzureBlobConfigurationTestConfiguration {
private BlobServiceClient blobServiceClient;
@Mock
- private BlobContainerClient containerClient;
+ private BlobContainerClient photoContainerClient;
+
+ @Mock
+ private BlobContainerClient csvContainerClient;
@Mock
private BlobClient blobClient;
@@ -27,8 +32,13 @@ public AzureBlobConfigurationTestConfiguration() {
}
@Bean
- public BlobContainerClient blobContainerClient() {
- return containerClient;
+ public BlobContainerClient photoBlobContainerClient() {
+ return photoContainerClient;
+ }
+
+ @Bean
+ public BlobContainerClient csvBlobContainerClient() {
+ return csvContainerClient;
}
@Bean
@@ -40,4 +50,14 @@ public BlobClient blobClient() {
public BlobServiceClient blobServiceClient() {
return blobServiceClient;
}
+
+ @Bean
+ public AzureBlobService photoAzureBlobService() {
+ return new AzureBlobService(photoContainerClient);
+ }
+
+ @Bean
+ public AzureBlobService csvAzureBlobService() {
+ return new AzureBlobService(csvContainerClient);
+ }
}
diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java
index e713e53a..643b0e4c 100644
--- a/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java
+++ b/src/integrationTest/java/uk/gov/hmcts/reform/fact/data/api/controllers/CsvControllerTest.java
@@ -9,12 +9,18 @@
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
-import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.CsvCreationException;
-import uk.gov.hmcts.reform.fact.data.api.services.CsvService;
+import uk.gov.hmcts.reform.fact.data.api.clients.SlackClient;
+import uk.gov.hmcts.reform.fact.data.api.services.AzureBlobService;
+import uk.gov.hmcts.reform.fact.data.api.services.CourtDetailsViewService;
+import uk.gov.hmcts.reform.fact.data.api.services.CourtService;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
+import java.util.Collections;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -29,24 +35,35 @@ class CsvControllerTest {
@Autowired
private MockMvc mvc;
+ @MockitoBean(name = "csvAzureBlobService")
+ private AzureBlobService azureBlobService;
+
+ @MockitoBean
+ private CourtService courtService;
+
@MockitoBean
- private CsvService csvService;
+ private CourtDetailsViewService courtDetailsViewService;
+
+ @MockitoBean
+ private SlackClient slackClient;
@Test
@DisplayName("POST /csv/ returns 200 when CSV is created and uploaded successfully")
void createAndUploadCsvReturns200() throws Exception {
- doNothing().when(csvService).createAndUploadCsv();
+ when(courtService.getAllCourtDetails()).thenReturn(Collections.emptyList());
+ when(azureBlobService.uploadFile(anyString(), any())).thenReturn("http://example.com/blob");
mvc.perform(post("/csv/"))
.andExpect(status().isOk());
+
+ verify(azureBlobService, times(1)).uploadFile(anyString(), any());
}
@Test
- @DisplayName("POST /csv/ returns 502 when Azure upload fails")
+ @DisplayName("POST /csv/ returns 502 when Azure upload fails (e.g. container missing)")
void createAndUploadCsvReturns502OnAzureUploadException() throws Exception {
- doThrow(new AzureUploadException("Failed to upload CSV file to Azure Blob Storage"))
- .when(csvService)
- .createAndUploadCsv();
+ when(courtService.getAllCourtDetails()).thenReturn(Collections.emptyList());
+ when(azureBlobService.uploadFile(anyString(), any())).thenThrow(new RuntimeException("Container not found"));
mvc.perform(post("/csv/"))
.andExpect(status().isBadGateway())
@@ -56,9 +73,7 @@ void createAndUploadCsvReturns502OnAzureUploadException() throws Exception {
@Test
@DisplayName("POST /csv/ returns 500 when CSV creation fails")
void createAndUploadCsvReturns500OnCsvCreationException() throws Exception {
- doThrow(new CsvCreationException("Failed to create CSV file"))
- .when(csvService)
- .createAndUploadCsv();
+ when(courtService.getAllCourtDetails()).thenThrow(new RuntimeException("DB error"));
mvc.perform(post("/csv/"))
.andExpect(status().isInternalServerError())
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfiguration.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfiguration.java
index b56c05ce..9ab90fff 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfiguration.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfiguration.java
@@ -7,14 +7,36 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
+import uk.gov.hmcts.reform.fact.data.api.services.AzureBlobService;
+import org.springframework.beans.factory.annotation.Qualifier;
+
@Configuration
@Profile("!test")
public class AzureBlobConfiguration {
@Bean
- public BlobContainerClient blobContainerClient(
+ public BlobContainerClient photoBlobContainerClient(
+ BlobServiceClient blobServiceClient,
+ @Value("${spring.cloud.azure.storage.blob.photo-container-name}") String containerName) {
+ return blobServiceClient.getBlobContainerClient(containerName);
+ }
+
+ @Bean
+ public BlobContainerClient csvBlobContainerClient(
BlobServiceClient blobServiceClient,
- @Value("${spring.cloud.azure.storage.blob.container-name}") String containerName) {
+ @Value("${spring.cloud.azure.storage.blob.csv-container-name}") String containerName) {
return blobServiceClient.getBlobContainerClient(containerName);
}
+
+ @Bean
+ public AzureBlobService photoAzureBlobService(
+ @Qualifier("photoBlobContainerClient") BlobContainerClient photoBlobContainerClient) {
+ return new AzureBlobService(photoBlobContainerClient);
+ }
+
+ @Bean
+ public AzureBlobService csvAzureBlobService(
+ @Qualifier("csvBlobContainerClient") BlobContainerClient csvBlobContainerClient) {
+ return new AzureBlobService(csvBlobContainerClient);
+ }
}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java
index 68ed38a9..d27db0f7 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/models/StringMultipartFile.java
@@ -1,61 +1,57 @@
package uk.gov.hmcts.reform.fact.data.api.models;
+import lombok.Getter;
import org.springframework.web.multipart.MultipartFile;
-
import java.io.ByteArrayInputStream;
import java.io.InputStream;
+/**
+ * An implementation of the {@link MultipartFile} interface that wraps a {@link String} content.
+ *
+ * This class is used to represent a file created from a string, which can be then treated as a standard
+ * multipart file for operations like uploading to cloud storage.
+ */
+@Getter
public class StringMultipartFile implements MultipartFile {
private final String name;
- private final String originalFileName;
+ private final String originalFilename;
private final String contentType;
- private final byte[] content;
-
- public StringMultipartFile(String name, String originalFileName, String contentType, String content) {
+ private final byte[] bytes;
+
+ /**
+ * Constructs a new StringMultipartFile.
+ *
+ * @param name the name of the file
+ * @param originalFilename the original filename
+ * @param contentType the content type of the file
+ * @param content the string content of the file
+ */
+ public StringMultipartFile(String name, String originalFilename, String contentType, String content) {
this.name = name;
- this.originalFileName = originalFileName;
+ this.originalFilename = originalFilename;
this.contentType = contentType;
- this.content = content.getBytes();
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public String getOriginalFilename() {
- return originalFileName;
- }
-
- @Override
- public String getContentType() {
- return contentType;
+ this.bytes = content.getBytes();
}
@Override
public boolean isEmpty() {
- return content.length == 0;
+ return bytes.length == 0;
}
@Override
public long getSize() {
- return content.length;
- }
-
- @Override
- public byte[] getBytes() {
- return content;
+ return bytes.length;
}
@Override
public InputStream getInputStream() {
- return new ByteArrayInputStream(content);
+ return new ByteArrayInputStream(bytes);
}
@Override
public void transferTo(java.io.File dest) throws IllegalStateException {
- // Optionally, you can implement the logic to transfer the content to a file here.
+ // This implementation is currently empty as it's not required for current use cases,
+ // but the override is necessary for the interface.
}
}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java
index 56fc2bdf..6da2026f 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobService.java
@@ -2,80 +2,58 @@
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
-import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.models.BlobHttpHeaders;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.AzureUploadException;
import java.io.IOException;
@Slf4j
-@Service
public class AzureBlobService {
private final BlobContainerClient blobContainerClient;
- private final BlobServiceClient blobServiceClient;
- public AzureBlobService(BlobContainerClient blobContainerClient, BlobServiceClient blobServiceClient) {
+ public AzureBlobService(BlobContainerClient blobContainerClient) {
this.blobContainerClient = blobContainerClient;
- this.blobServiceClient = blobServiceClient;
}
/**
- * Uploads the image in the Azure blob service.
+ * Uploads a file to an Azure blob container.
+ * If containerName is provided, it uploads to that container and creates it if it does not exist.
*
- * @param imageId The identifier of the image.
- * @param file The file to upload.
- * @return The id linked to the uploaded image.
+ * @param blobName The name of the blob to create.
+ * @param file The file to upload.
+ * @return The URL of the uploaded blob.
*/
- public String uploadFile(String imageId, MultipartFile file) {
- BlobClient blobClient = blobContainerClient.getBlobClient(imageId);
+ public String uploadFile(String blobName, MultipartFile file) {
- try {
- blobClient.upload(file.getInputStream(), file.getSize(), true);
-
- BlobHttpHeaders headers = new BlobHttpHeaders()
- .setContentType(file.getContentType());
-
- blobClient.setHttpHeaders(headers);
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
+ uploadToBlob(blobClient, file);
- } catch (IOException e) {
- throw new AzureUploadException("Could not upload provided file to Azure");
- }
+ log.info("Uploaded file {} to {}", file.getOriginalFilename(), blobClient.getBlobUrl());
return blobClient.getBlobUrl();
}
- public void uploadFile(String containerName, String blobName, MultipartFile file) {
- BlobContainerClient containerClient = blobServiceClient.getBlobContainerClient(containerName);
- if (!containerClient.exists()) {
- containerClient.create();
- log.info("Created Azure blob container {}", containerName);
- }
- BlobClient blobClient = containerClient.getBlobClient(blobName);
+ private void uploadToBlob(BlobClient blobClient, MultipartFile file) {
try {
blobClient.upload(file.getInputStream(), file.getSize(), true);
-
BlobHttpHeaders headers = new BlobHttpHeaders()
.setContentType(file.getContentType());
-
blobClient.setHttpHeaders(headers);
- log.info("Uploaded file {} to {}", file.getOriginalFilename(), containerName);
-
} catch (IOException e) {
throw new AzureUploadException("Could not upload provided file to Azure");
}
}
/**
- * Delete a blob from the blob store by the imageId.
+ * Delete a blob from the blob store by the blob name.
*
- * @param imageId The identifier of the image.
+ * @param blobName The name of the blob to delete.
*/
- public void deleteBlob(String imageId) {
- BlobClient blobClient = blobContainerClient.getBlobClient(imageId);
+ public void deleteBlob(String blobName) {
+ BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
blobClient.delete();
}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoService.java
index 95f12558..01d312fa 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoService.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoService.java
@@ -9,6 +9,8 @@
import java.util.UUID;
+import org.springframework.beans.factory.annotation.Qualifier;
+
@Slf4j
@Service
public class CourtPhotoService {
@@ -18,7 +20,8 @@ public class CourtPhotoService {
private final AzureBlobService azureBlobService;
public CourtPhotoService(CourtPhotoRepository courtPhotoRepository,
- CourtService courtService, AzureBlobService azureBlobService) {
+ CourtService courtService,
+ @Qualifier("photoAzureBlobService") AzureBlobService azureBlobService) {
this.courtPhotoRepository = courtPhotoRepository;
this.courtService = courtService;
this.azureBlobService = azureBlobService;
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
index ce506573..d9d7c400 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
@@ -2,7 +2,6 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import uk.gov.hmcts.reform.fact.data.api.clients.SlackClient;
@@ -17,6 +16,8 @@
import java.util.ArrayList;
import java.util.List;
+import org.springframework.beans.factory.annotation.Qualifier;
+
/**
* Service for handling CSV file operations.
*/
@@ -27,7 +28,6 @@ public class CsvService {
private static final String CSV_FILE_NAME = "courts-and-tribunals-data.csv";
private static final String CSV_CONTENT_TYPE = "text/csv";
- private final String csvContainerName;
private final CourtService courtService;
private final CourtDetailsViewService courtDetailsViewService;
private final AzureBlobService azureBlobService;
@@ -36,16 +36,14 @@ public class CsvService {
public CsvService(CourtService courtService,
CourtDetailsViewService courtDetailsViewService,
- AzureBlobService azureBlobService,
+ @Qualifier("csvAzureBlobService") AzureBlobService azureBlobService,
ObjectMapper objectMapper,
- SlackClient slackClient,
- @Value("${fact.data-api.csv-container-name:csv}") String csvContainerName) {
+ SlackClient slackClient) {
this.courtService = courtService;
this.courtDetailsViewService = courtDetailsViewService;
this.azureBlobService = azureBlobService;
this.objectMapper = objectMapper;
this.slackClient = slackClient;
- this.csvContainerName = csvContainerName;
}
public void createAndUploadCsv() {
@@ -61,7 +59,7 @@ public void createAndUploadCsv() {
public void uploadCsvToAzureBlob(List actions, StringMultipartFile stringMultipartFile) {
try {
azureBlobService
- .uploadFile(csvContainerName, CSV_FILE_NAME, stringMultipartFile);
+ .uploadFile(CSV_FILE_NAME, stringMultipartFile);
} catch (Exception e) {
log.error("Error while uploading CSV", e);
actions.add("Failed to upload CSV file to Azure Blob Storage. Check App insights.");
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index a143d803..364ddfda 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -69,7 +69,8 @@ spring:
storage:
account-name: ${AZURE_STORAGE_ACCOUNT_NAME}
blob:
- container-name: photos
+ photo-container-name: photos
+ csv-container-name: csv
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
# required for security flows
active-directory:
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTest.java
index 38065697..155bb085 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/config/AzureBlobConfigurationTest.java
@@ -30,8 +30,19 @@ void setUp() {
}
@Test
- void testBlobContainerClientBean() {
- BlobContainerClient client = azureBlobConfiguration.blobContainerClient(
+ void testPhotoBlobContainerClientBean() {
+ BlobContainerClient client = azureBlobConfiguration.photoBlobContainerClient(
+ mockBlobServiceClient, containerName
+ );
+
+ assertEquals(mockBlobContainerClient, client);
+ verify(mockBlobServiceClient, times(1))
+ .getBlobContainerClient(containerName);
+ }
+
+ @Test
+ void testCsvBlobContainerClientBean() {
+ BlobContainerClient client = azureBlobConfiguration.csvBlobContainerClient(
mockBlobServiceClient, containerName
);
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java
index c9e8c380..41edc8eb 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java
@@ -4,6 +4,7 @@
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.models.BlobHttpHeaders;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -28,7 +29,8 @@
class AzureBlobServiceTest {
private static final String IMAGE_ID = "test-image-id";
- private static final String CONTAINER_NAME = "csv";
+ private static final String CSV_CONTAINER_NAME = "csv";
+ private static final String PHOTOS_CONTAINER_NAME = "photos";
private static final String CONTENT_TYPE = "image/jpeg";
private static final String BLOB_URL = "https://example.com/blob/test-image-id";
@@ -47,12 +49,16 @@ class AzureBlobServiceTest {
@InjectMocks
private AzureBlobService azureBlobService;
+ @BeforeEach
+ void setUp() {
+ when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
+ }
+
@Test
void uploadFileShouldUploadAndReturnUrl() throws IOException {
byte[] fileBytes = "dummy payload".getBytes(StandardCharsets.UTF_8);
InputStream inputStream = new ByteArrayInputStream(fileBytes);
- when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
when(multipartFile.getInputStream()).thenReturn(inputStream);
when(multipartFile.getSize()).thenReturn((long) fileBytes.length);
when(multipartFile.getContentType()).thenReturn(CONTENT_TYPE);
@@ -71,7 +77,6 @@ void uploadFileShouldUploadAndReturnUrl() throws IOException {
@Test
void uploadFileShouldThrowAzureUploadExceptionWhenReadingFails() throws IOException {
- when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
when(multipartFile.getInputStream()).thenThrow(new IOException("Read failure"));
assertThrows(AzureUploadException.class, () ->
@@ -84,8 +89,6 @@ void uploadFileShouldThrowAzureUploadExceptionWhenReadingFails() throws IOExcept
@Test
void deleteBlobShouldDeleteBlobById() {
- when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
-
azureBlobService.deleteBlob(IMAGE_ID);
verify(blobClient).delete();
@@ -96,15 +99,14 @@ void uploadFileWithContainerShouldUploadToProvidedContainer() throws IOException
byte[] fileBytes = "dummy payload".getBytes(StandardCharsets.UTF_8);
InputStream inputStream = new ByteArrayInputStream(fileBytes);
- when(blobServiceClient.getBlobContainerClient(CONTAINER_NAME)).thenReturn(blobContainerClient);
- when(blobContainerClient.exists()).thenReturn(true);
- when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
when(multipartFile.getInputStream()).thenReturn(inputStream);
when(multipartFile.getSize()).thenReturn((long) fileBytes.length);
when(multipartFile.getContentType()).thenReturn(CONTENT_TYPE);
+ when(blobClient.getBlobUrl()).thenReturn(BLOB_URL);
- azureBlobService.uploadFile(CONTAINER_NAME, IMAGE_ID, multipartFile);
+ String result = azureBlobService.uploadFile(IMAGE_ID, multipartFile);
+ assertThat(result).isEqualTo(BLOB_URL);
verify(blobClient).upload(inputStream, fileBytes.length, true);
verify(blobClient).setHttpHeaders(org.mockito.ArgumentMatchers.any(BlobHttpHeaders.class));
}
@@ -114,29 +116,24 @@ void uploadFileWithContainerShouldCreateContainerWhenMissing() throws IOExceptio
byte[] fileBytes = "dummy payload".getBytes(StandardCharsets.UTF_8);
InputStream inputStream = new ByteArrayInputStream(fileBytes);
- when(blobServiceClient.getBlobContainerClient(CONTAINER_NAME)).thenReturn(blobContainerClient);
- when(blobContainerClient.exists()).thenReturn(false);
- when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
when(multipartFile.getInputStream()).thenReturn(inputStream);
when(multipartFile.getSize()).thenReturn((long) fileBytes.length);
when(multipartFile.getContentType()).thenReturn(CONTENT_TYPE);
+ when(blobClient.getBlobUrl()).thenReturn(BLOB_URL);
- azureBlobService.uploadFile(CONTAINER_NAME, IMAGE_ID, multipartFile);
+ String result = azureBlobService.uploadFile(IMAGE_ID, multipartFile);
- verify(blobContainerClient).create();
+ assertThat(result).isEqualTo(BLOB_URL);
verify(blobClient).upload(inputStream, fileBytes.length, true);
verify(blobClient).setHttpHeaders(org.mockito.ArgumentMatchers.any(BlobHttpHeaders.class));
}
@Test
void uploadFileWithContainerShouldThrowAzureUploadExceptionWhenReadingFails() throws IOException {
- when(blobServiceClient.getBlobContainerClient(CONTAINER_NAME)).thenReturn(blobContainerClient);
- when(blobContainerClient.exists()).thenReturn(true);
- when(blobContainerClient.getBlobClient(IMAGE_ID)).thenReturn(blobClient);
when(multipartFile.getInputStream()).thenThrow(new IOException("Read failure"));
assertThrows(AzureUploadException.class, () ->
- azureBlobService.uploadFile(CONTAINER_NAME, IMAGE_ID, multipartFile)
+ azureBlobService.uploadFile(IMAGE_ID, multipartFile)
);
verify(blobClient, never()).setHttpHeaders(org.mockito.ArgumentMatchers.any(BlobHttpHeaders.class));
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoServiceTest.java
index c165e6fd..7a294867 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CourtPhotoServiceTest.java
@@ -23,6 +23,8 @@
@ExtendWith(MockitoExtension.class)
class CourtPhotoServiceTest {
+ private static final String CONTAINER_NAME = "photos";
+
@Mock
private CourtPhotoRepository courtPhotoRepository;
@@ -76,8 +78,10 @@ void setCourtPhotoShouldCreateNewWhenNoneExists() {
when(courtService.getCourtById(courtId)).thenReturn(null);
when(courtPhotoRepository.findCourtPhotoByCourtId(courtId)).thenReturn(Optional.empty());
- when(azureBlobService.uploadFile(eq(courtId.toString()), eq(multipartFile))).thenReturn(uploadedLink);
- when(courtPhotoRepository.save(any(CourtPhoto.class))).thenAnswer(inv -> inv.getArgument(0));
+ when(azureBlobService
+ .uploadFile(eq(courtId.toString()), eq(multipartFile))).thenReturn(uploadedLink);
+ when(courtPhotoRepository
+ .save(any(CourtPhoto.class))).thenAnswer(inv -> inv.getArgument(0));
CourtPhoto result = courtPhotoService.setCourtPhoto(courtId, multipartFile);
@@ -95,8 +99,10 @@ void setCourtPhotoShouldUpdateExistingPhoto() {
when(courtService.getCourtById(courtId)).thenReturn(null);
when(courtPhotoRepository.findCourtPhotoByCourtId(courtId)).thenReturn(Optional.of(existing));
- when(azureBlobService.uploadFile(eq(courtId.toString()), eq(multipartFile))).thenReturn("new-link");
- when(courtPhotoRepository.save(any(CourtPhoto.class))).thenAnswer(inv -> inv.getArgument(0));
+ when(azureBlobService
+ .uploadFile(eq(courtId.toString()), eq(multipartFile))).thenReturn("new-link");
+ when(courtPhotoRepository
+ .save(any(CourtPhoto.class))).thenAnswer(inv -> inv.getArgument(0));
CourtPhoto result = courtPhotoService.setCourtPhoto(courtId, multipartFile);
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
index 0a401cea..9fcdf58e 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
@@ -26,7 +26,7 @@
@ExtendWith(MockitoExtension.class)
class CsvServiceTest {
- private static final String CSV_CONTAINER_NAME = "csv-container";
+ private static final String CSV_CONTAINER_NAME = "csv";
private static final String CSV_FILE_NAME = "courts-and-tribunals-data.csv";
@Mock
@@ -51,7 +51,7 @@ void uploadCsvToAzureBlobShouldUseConfiguredContainerName() {
csvService.uploadCsvToAzureBlob(actions, csvFile);
- verify(azureBlobService).uploadFile(CSV_CONTAINER_NAME, CSV_FILE_NAME, csvFile);
+ verify(azureBlobService).uploadFile(CSV_FILE_NAME, csvFile);
assertThat(actions).isEmpty();
}
@@ -63,7 +63,7 @@ void uploadCsvToAzureBlobShouldAppendActionAndThrowWhenUploadFails() {
doThrow(new RuntimeException("azure failure"))
.when(azureBlobService)
- .uploadFile(CSV_CONTAINER_NAME, CSV_FILE_NAME, csvFile);
+ .uploadFile(CSV_FILE_NAME, csvFile);
AzureUploadException exception = assertThrows(AzureUploadException.class, () ->
csvService.uploadCsvToAzureBlob(actions, csvFile)
@@ -80,8 +80,8 @@ void createAndUploadCsvShouldCreateAndUploadWithoutSlackMessageOnSuccess() {
csvService.createAndUploadCsv();
- verify(azureBlobService).uploadFile(org.mockito.ArgumentMatchers.eq(CSV_CONTAINER_NAME),
- org.mockito.ArgumentMatchers.eq(CSV_FILE_NAME), any(StringMultipartFile.class));
+ verify(azureBlobService)
+ .uploadFile(org.mockito.ArgumentMatchers.eq(CSV_FILE_NAME), any(StringMultipartFile.class));
verify(slackClient, never()).sendSlackMessage(any());
}
@@ -102,8 +102,7 @@ void createAndUploadCsvShouldSendSlackMessageAndThrowWhenUploadFails() {
when(courtService.getAllCourtDetails()).thenReturn(Collections.emptyList());
doThrow(new RuntimeException("azure failure"))
.when(azureBlobService)
- .uploadFile(org.mockito.ArgumentMatchers.eq(CSV_CONTAINER_NAME),
- org.mockito.ArgumentMatchers.eq(CSV_FILE_NAME), any(StringMultipartFile.class));
+ .uploadFile(org.mockito.ArgumentMatchers.eq(CSV_FILE_NAME), any(StringMultipartFile.class));
AzureUploadException exception = assertThrows(AzureUploadException.class, csvService::createAndUploadCsv);
@@ -114,6 +113,6 @@ void createAndUploadCsvShouldSendSlackMessageAndThrowWhenUploadFails() {
private CsvService buildService() {
return new CsvService(
- courtService, courtDetailsViewService, azureBlobService, objectMapper, slackClient, CSV_CONTAINER_NAME);
+ courtService, courtDetailsViewService, azureBlobService, objectMapper, slackClient);
}
}
From 1c537af1e9670d87e7bc11284be7a5db911bbe10 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Wed, 20 May 2026 16:48:26 +0100
Subject: [PATCH 16/18] fix jackson dependency after merge
---
.../fact/data/api/config/JacksonConfiguration.java | 14 ++++++++++++++
.../data/api/services/AzureBlobServiceTest.java | 2 --
.../fact/data/api/services/CsvServiceTest.java | 1 -
3 files changed, 14 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java
new file mode 100644
index 00000000..08c3ff02
--- /dev/null
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java
@@ -0,0 +1,14 @@
+package uk.gov.hmcts.reform.fact.data.api.config;
+
+import com.fasterxml.jackson.datatype.hibernate7.Hibernate7Module;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class JacksonConfiguration {
+
+ @Bean
+ public Hibernate7Module hibernate7Module() {
+ return new Hibernate7Module();
+ }
+}
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java
index 41edc8eb..8ea4ad9e 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/AzureBlobServiceTest.java
@@ -29,8 +29,6 @@
class AzureBlobServiceTest {
private static final String IMAGE_ID = "test-image-id";
- private static final String CSV_CONTAINER_NAME = "csv";
- private static final String PHOTOS_CONTAINER_NAME = "photos";
private static final String CONTENT_TYPE = "image/jpeg";
private static final String BLOB_URL = "https://example.com/blob/test-image-id";
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
index 9fcdf58e..fe1e0e8b 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
@@ -26,7 +26,6 @@
@ExtendWith(MockitoExtension.class)
class CsvServiceTest {
- private static final String CSV_CONTAINER_NAME = "csv";
private static final String CSV_FILE_NAME = "courts-and-tribunals-data.csv";
@Mock
From ba5b9be6f4fd76d6b058cd4c0ae8f8d3220b5829 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Thu, 21 May 2026 12:58:22 +0100
Subject: [PATCH 17/18] Update jackson
---
build.gradle | 3 +--
.../data/api/config/JacksonConfiguration.java | 14 --------------
.../fact/data/api/services/CsvService.java | 2 +-
.../reform/fact/data/api/utils/CsvUtil.java | 17 +++++++++--------
.../fact/data/api/services/CsvServiceTest.java | 5 +++--
.../reform/fact/data/api/utils/CsvUtilTest.java | 16 +++++++++-------
6 files changed, 23 insertions(+), 34 deletions(-)
delete mode 100644 src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java
diff --git a/build.gradle b/build.gradle
index 60b8350e..80789e18 100644
--- a/build.gradle
+++ b/build.gradle
@@ -195,8 +195,7 @@ dependencies {
implementation group: 'com.azure.spring', name: 'spring-cloud-azure-starter-storage-blob', version: '7.2.0'
- implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-hibernate7', version: '2.21.3'
- implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-csv', version: '2.21.3'
+ implementation group: 'tools.jackson.dataformat', name: 'jackson-dataformat-csv'
implementation group: 'tools.jackson.datatype', name: 'jackson-datatype-hibernate7'
implementation group: 'com.github.hmcts.java-logging', name: 'logging', version: '8.0.0'
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java
deleted file mode 100644
index 08c3ff02..00000000
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/config/JacksonConfiguration.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package uk.gov.hmcts.reform.fact.data.api.config;
-
-import com.fasterxml.jackson.datatype.hibernate7.Hibernate7Module;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-public class JacksonConfiguration {
-
- @Bean
- public Hibernate7Module hibernate7Module() {
- return new Hibernate7Module();
- }
-}
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
index d9d7c400..b0a96441 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/services/CsvService.java
@@ -1,6 +1,6 @@
package uk.gov.hmcts.reform.fact.data.api.services;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
diff --git a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
index 935824bd..47e9ee3f 100644
--- a/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
+++ b/src/main/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtil.java
@@ -1,10 +1,10 @@
package uk.gov.hmcts.reform.fact.data.api.utils;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.dataformat.csv.CsvMapper;
-import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.dataformat.csv.CsvMapper;
+import tools.jackson.dataformat.csv.CsvSchema;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
import java.util.ArrayList;
@@ -56,8 +56,9 @@ protected CsvUtil(CsvMapper csvMapper) {
}
public CsvUtil() {
- this.csvMapper = new CsvMapper();
- this.csvMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ this.csvMapper = CsvMapper.builder()
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .build();
}
public String convertJsonToCsv(JsonNode jsonArrayNode) {
@@ -70,7 +71,7 @@ public String convertJsonToCsv(JsonNode jsonArrayNode) {
try {
return csvMapper.writer(schema).writeValueAsString(flatList);
- } catch (RuntimeException | JsonProcessingException ex) {
+ } catch (JacksonException ex) {
throw new JsonConvertException("Failed to convert JSON to CSV: " + ex.getMessage());
}
}
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
index fe1e0e8b..ef7fadec 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/services/CsvServiceTest.java
@@ -1,6 +1,7 @@
package uk.gov.hmcts.reform.fact.data.api.services;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -40,7 +41,7 @@ class CsvServiceTest {
@Mock
private SlackClient slackClient;
- private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ObjectMapper objectMapper = JsonMapper.builder().build();
@Test
void uploadCsvToAzureBlobShouldUseConfiguredContainerName() {
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
index 384c1b82..4bdc3119 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
@@ -1,9 +1,11 @@
package uk.gov.hmcts.reform.fact.data.api.utils;
-import com.fasterxml.jackson.dataformat.csv.CsvMapper;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
+import tools.jackson.core.JacksonException;
+import tools.jackson.dataformat.csv.CsvMapper;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+import tools.jackson.databind.node.ArrayNode;
+import tools.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import uk.gov.hmcts.reform.fact.data.api.errorhandling.exceptions.JsonConvertException;
@@ -17,7 +19,7 @@
class CsvUtilTest {
- private final ObjectMapper mapper = new ObjectMapper();
+ private final ObjectMapper mapper = JsonMapper.builder().build();
private final CsvUtil csvUtil = new CsvUtil();
@Test
@@ -295,8 +297,8 @@ void shouldThrowJsonConvertExceptionWhenCsvWritingFails() throws Exception {
CsvUtil utilWithMock = new CsvUtil(mockCsvMapper);
when(mockCsvMapper.writer(org.mockito.ArgumentMatchers.any(
- com.fasterxml.jackson.dataformat.csv.CsvSchema.class)))
- .thenThrow(new RuntimeException("Mock failure"));
+ tools.jackson.dataformat.csv.CsvSchema.class)))
+ .thenThrow(new JacksonException("Mock failure") {});
ArrayNode root = mapper.createArrayNode();
root.addObject();
From aa731a5ebafbee8ddc099f15f89b99693568dd17 Mon Sep 17 00:00:00 2001
From: Rebecca <67150587+RebeccaHayleyPickles@users.noreply.github.com>
Date: Thu, 21 May 2026 16:21:06 +0100
Subject: [PATCH 18/18] Update CsvUtilTest.java
---
.../fact/data/api/utils/CsvUtilTest.java | 40 ++++++++++---------
1 file changed, 22 insertions(+), 18 deletions(-)
diff --git a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
index 4bdc3119..05771e09 100644
--- a/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
+++ b/src/test/java/uk/gov/hmcts/reform/fact/data/api/utils/CsvUtilTest.java
@@ -95,7 +95,7 @@ void shouldHandleOpenOnCath() {
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("open")).isEqualTo(true);
+ assertThat(result).containsEntry("open", true);
}
@Test
@@ -165,8 +165,9 @@ void shouldFlattenFieldsOfLawWithAreasAndCourts() {
Map result = csvUtil.flattenCourtNode(root);
String addressesStr = result.get("addresses").toString();
- assertThat(addressesStr).contains("Areas of Law: Family");
- assertThat(addressesStr).contains("Courts: High Court");
+ assertThat(addressesStr)
+ .contains("Areas of Law: Family")
+ .contains("Courts: High Court");
}
@Test
@@ -184,8 +185,9 @@ void shouldFlattenFieldsOfLawWithTextualAreasAndCourts() {
Map result = csvUtil.flattenCourtNode(root);
String addressesStr = result.get("addresses").toString();
- assertThat(addressesStr).contains("Areas of Law: Family");
- assertThat(addressesStr).contains("Courts: High Court");
+ assertThat(addressesStr)
+ .contains("Areas of Law: Family")
+ .contains("Courts: High Court");
}
@Test
@@ -202,8 +204,9 @@ void shouldFallbackToAddressNodeWhenFieldsOfLawNodeMissing() {
Map result = csvUtil.flattenCourtNode(root);
String addressesStr = result.get("addresses").toString();
- assertThat(addressesStr).contains("Areas of Law: Crime");
- assertThat(addressesStr).contains("Courts: Crown Court");
+ assertThat(addressesStr)
+ .contains("Areas of Law: Crime")
+ .contains("Courts: Crown Court");
}
@Test
@@ -217,8 +220,9 @@ void shouldConvertJsonToCsv() {
assertThat(csv)
.contains("name,lat,lon,number,cci_code,magistrate_code,slug,types,open,dx_number,areas_of_law,addresses");
- assertThat(csv).contains("Test Court");
- assertThat(csv).contains("test-court");
+ assertThat(csv)
+ .contains("Test Court")
+ .contains("test-court");
}
@Test
@@ -246,14 +250,14 @@ void shouldHandleDxCodes() {
root.put("dx_number", "DX 123");
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("dx_number")).isEqualTo("DX 123");
+ assertThat(result).containsEntry("dx_number", "DX 123");
ArrayNode dxCodes = root.putArray("courtDxCodes");
ObjectNode dx1 = dxCodes.addObject();
dx1.put("dxCode", "DX 456");
result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("dx_number")).isEqualTo("DX 456");
+ assertThat(result).containsEntry("dx_number", "DX 456");
}
@Test
@@ -273,12 +277,12 @@ void shouldReadNumbersAndBooleansCorrectly() {
address.put("lon", -0.1);
Map result = csvUtil.flattenCourtNode(root);
- assertThat(result.get("open")).isEqualTo(true);
- assertThat(result.get("number")).isEqualTo(123);
- assertThat(result.get("cci_code")).isEqualTo(456);
- assertThat(result.get("magistrate_code")).isEqualTo(789);
- assertThat(result.get("lat")).isEqualTo(51.5);
- assertThat(result.get("lon")).isEqualTo(-0.1);
+ assertThat(result).containsEntry("open",true);
+ assertThat(result).containsEntry("number", 123);
+ assertThat(result).containsEntry("cci_code", 456);
+ assertThat(result).containsEntry("magistrate_code", 789);
+ assertThat(result).containsEntry("lat", 51.5);
+ assertThat(result).containsEntry("lon", -0.1);
}
@Test
@@ -288,7 +292,7 @@ void shouldHandleNullsInReadMethods() {
assertThat(result.get("lat")).isNull();
assertThat(result.get("number")).isNull();
- assertThat(result.get("open")).isEqualTo(false);
+ assertThat(result).containsEntry("open", false);
}
@Test