From b4ce867efc84e300ede17cc602f9771f538cc5bc Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Thu, 25 Dec 2025 14:15:36 +0100 Subject: [PATCH] issue #6 - ResponseResultInfo#Errors: wrong object structure issue #5 - Add batch DNS record operations and refactor Cloudflare API client. --- .../codes/thischwa/cf/CfBasicHttpClient.java | 40 +++++++------ .../java/codes/thischwa/cf/CfDnsClient.java | 60 ++++++++++++++++--- .../java/codes/thischwa/cf/CfRequest.java | 7 +++ .../codes/thischwa/cf/model/BatchEntry.java | 39 ++++++++++++ .../thischwa/cf/model/BatchResponse.java | 8 +++ .../codes/thischwa/cf/model/RecordEntity.java | 28 +++++++-- .../thischwa/cf/model/ResponseResultInfo.java | 22 ++++++- .../java/codes/thischwa/cf/CfClientTest.java | 56 +++++++++++++++-- .../codes/thischwa/cf/ObjectMapperTest.java | 43 +++++++++++-- .../thischwa/cf/ResponseValidatorTest.java | 20 ++++--- 10 files changed, 274 insertions(+), 49 deletions(-) create mode 100644 src/main/java/codes/thischwa/cf/model/BatchEntry.java create mode 100644 src/main/java/codes/thischwa/cf/model/BatchResponse.java diff --git a/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java b/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java index 0d28e1b..9fc7c14 100644 --- a/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java +++ b/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java @@ -1,12 +1,10 @@ package codes.thischwa.cf; -import codes.thischwa.cf.model.AbstractEntity; import codes.thischwa.cf.model.AbstractResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; import lombok.extern.slf4j.Slf4j; - import org.apache.hc.client5.http.classic.methods.HttpDelete; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPatch; @@ -70,14 +68,14 @@ abstract class CfBasicHttpClient { EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8))); + T respObj = objectMapper.readValue(result.responseBody, responseType); + if (!respObj.getResponseResultInfo().isSuccess()) { + log.error("API error."); + respObj.getResponseResultInfo().getErrors().forEach(e -> log.error(" - {}", e.toString())); + throw new CloudflareApiException("API error."); + } logUri = request.getRequestUri(); if (result.statusCode >= 200 && result.statusCode < 300) { - T respObj = objectMapper.readValue(result.responseBody, responseType); - if (!respObj.getResponseResultInfo().isSuccess()) { - log.error("API error."); - respObj.getResponseResultInfo().getErrors().forEach(e -> log.error(" - {}", e)); - throw new CloudflareApiException("API error."); - } return respObj; } else { log.error("{} request failed for URL {}: Status {}", request.getMethod(), request.getUri(), @@ -110,23 +108,25 @@ abstract class CfBasicHttpClient { return executeRequest(request, (Class) codes.thischwa.cf.model.RecordSingleResponse.class); } + /** * Sends a POST request with a payload to the given endpoint and maps the response. */ - T postRequest(String endpoint, - R requestPayload) + T postRequest(String endpoint, + Object requestPayload, + Class responseType) throws CloudflareApiException { HttpPost request = new HttpPost(buildUrl(endpoint)); setRequestPayload(request, requestPayload); - return executeRequest(request, (Class) codes.thischwa.cf.model.RecordSingleResponse.class); + return executeRequest(request, responseType); } /** * Sends a PUT request with a payload to the given endpoint and maps the response. */ - T putRequest(String endpoint, - R requestPayload, - Class responseType) + T putRequest(String endpoint, + Object requestPayload, + Class responseType) throws CloudflareApiException { HttpPut request = new HttpPut(buildUrl(endpoint)); setRequestPayload(request, requestPayload); @@ -136,8 +136,8 @@ abstract class CfBasicHttpClient { /** * Sends a PATCH request with a payload to the given endpoint and maps the response. */ - T patchRequest(String endpoint, - R requestPayload) + T patchRequest(String endpoint, + Object requestPayload) throws CloudflareApiException { HttpPatch request = new HttpPatch(buildUrl(endpoint)); setRequestPayload(request, requestPayload); @@ -147,11 +147,13 @@ abstract class CfBasicHttpClient { /** * Sets the JSON payload for a request. */ - private void setRequestPayload(BasicClassicHttpRequest request, - R requestPayload) + private void setRequestPayload(BasicClassicHttpRequest request, + Object requestPayload) throws CloudflareApiException { try { - request.setEntity(new StringEntity(objectMapper.writeValueAsString(requestPayload), + String jsonPayload = objectMapper.writeValueAsString(requestPayload); + log.debug("Request payload: {}", jsonPayload); + request.setEntity(new StringEntity(jsonPayload, ContentType.APPLICATION_JSON)); } catch (JsonProcessingException e) { throw new CloudflareApiException("Error serializing JSON payload", e); diff --git a/src/main/java/codes/thischwa/cf/CfDnsClient.java b/src/main/java/codes/thischwa/cf/CfDnsClient.java index 6abc646..5e5c966 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClient.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClient.java @@ -1,6 +1,8 @@ package codes.thischwa.cf; import codes.thischwa.cf.model.AbstractResponse; +import codes.thischwa.cf.model.BatchEntry; +import codes.thischwa.cf.model.BatchResponse; import codes.thischwa.cf.model.PagingRequest; import codes.thischwa.cf.model.RecordEntity; import codes.thischwa.cf.model.RecordMultipleResponse; @@ -8,9 +10,11 @@ import codes.thischwa.cf.model.RecordSingleResponse; import codes.thischwa.cf.model.RecordType; import codes.thischwa.cf.model.ZoneEntity; import codes.thischwa.cf.model.ZoneMultipleResponse; +import java.util.ArrayList; import java.util.List; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.Nullable; /** * CfDnsClient is a client interface to interact with Cloudflare DNS service. It allows managing DNS @@ -97,7 +101,7 @@ public class CfDnsClient extends CfBasicHttpClient { * services. */ public CfDnsClient(boolean emptyResultThrowsException, String baseUrl, String authEmail, - String authKey) { + String authKey) { super(baseUrl, authEmail, authKey); this.responseValidator = new ResponseValidator(emptyResultThrowsException); } @@ -191,7 +195,7 @@ public class CfDnsClient extends CfBasicHttpClient { String fqdn = buildFqdn(zone, sld); String endpoint = CfRequest.RECORD_INFO_NAME_TYPE.buildPath(zone.getId(), fqdn, type); RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class); - checkResponse(resp, true); + checkResponse(resp, false); return resp.getResult().get(0); } @@ -209,7 +213,7 @@ public class CfDnsClient extends CfBasicHttpClient { * or creating the record. */ public RecordEntity recordCreateSld(ZoneEntity zone, String sld, int ttl, RecordType type, - String content) throws CloudflareApiException { + String content) throws CloudflareApiException { String fqdn = buildFqdn(zone, sld); return recordCreate(zone, fqdn, ttl, type, content); } @@ -226,7 +230,7 @@ public class CfDnsClient extends CfBasicHttpClient { * @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API */ public RecordEntity recordCreate(ZoneEntity zone, String name, int ttl, RecordType type, - String content) throws CloudflareApiException { + String content) throws CloudflareApiException { RecordEntity rec = RecordEntity.build(name, type, ttl, content); return recordCreate(zone, rec); } @@ -244,7 +248,7 @@ public class CfDnsClient extends CfBasicHttpClient { public RecordEntity recordCreate(ZoneEntity zone, RecordEntity rec) throws CloudflareApiException { String endpoint = CfRequest.RECORD_CREATE.buildPath(zone.getId()); - RecordSingleResponse resp = postRequest(endpoint, rec); + RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class); checkResponse(resp); log.info("Record {} of type {} successful created.", rec.getName(), rec.getType()); return resp.getResult(); @@ -262,7 +266,7 @@ public class CfDnsClient extends CfBasicHttpClient { public boolean recordDelete(ZoneEntity zone, RecordEntity rec) throws CloudflareApiException { boolean changed = recordDelete(zone, rec.getId()); if (changed) { - log.info("Record {} of the type {} successful deleted.", rec.getName(), rec.getType()); + log.debug("Record {} of the type [{}] successful deleted.", rec.getName(), rec.getType()); } else { log.warn("Record {} of the type {} was not deleted.", rec.getName(), rec.getType()); } @@ -323,13 +327,55 @@ public class CfDnsClient extends CfBasicHttpClient { try { RecordEntity rec = sldInfo(zone, sld, recordType); recordDelete(zone, rec); - log.info("Record {} of type {} successful deleted.", fqdn, recordTypes); + log.info("Record {} of type [{}] successful deleted.", fqdn, recordTypes); } catch (CloudflareNotFoundException e) { log.debug("Record {} of type {} does not exist.", fqdn, recordTypes); } } } + /** + * Records a batch of DNS record operations, including creating, updating, and deleting records + * within a specific zone. This method processes the provided put, patch, and delete operations + * into a clean format before sending a batch request to the Cloudflare API. + * + * @param zone the zone entity representing the DNS zone where the changes will be applied + * @param ttl the time-to-live (TTL) value assigned to the new records being added + * @param postRecords a list of records to be created; each record must contain the necessary + * attributes for creation + * @param patchRecords a list of records to be updated; only specific attributes (e.g., content) + * will be modified + * @param deleteRecords a list of records to be deleted; each record must contain name, type, and content + * @throws CloudflareApiException if there is an error while communicating with the Cloudflare API + */ + public void recordBatch(ZoneEntity zone, int ttl, @Nullable List postRecords, + @Nullable List patchRecords, @Nullable List deleteRecords) + throws CloudflareApiException { + BatchEntry batchEntry = new BatchEntry(); + // build 'clean' record entries + if (postRecords != null) { + List cleanedPosts = new ArrayList<>(); + postRecords.forEach( + rec -> cleanedPosts.add(RecordEntity.build(rec.getId(), rec.getName(), rec.getType(), rec.getTtl(), rec.getContent()))); + batchEntry.setPosts(cleanedPosts); + } + if (patchRecords != null) { + List cleanedPatches = new ArrayList<>(); + patchRecords.forEach(rec -> cleanedPatches.add(RecordEntity.build(rec.getId(), rec.getContent()))); + batchEntry.setPatches(cleanedPatches); + } + if (deleteRecords != null) { + List cleanedDeletes = new ArrayList<>(); + deleteRecords.forEach( + rec -> cleanedDeletes.add(RecordEntity.build(rec.getId(), rec.getName(), rec.getType(), null, rec.getContent()))); + batchEntry.setDeletes(cleanedDeletes); + } + + String endpoint = CfRequest.RECORD_BATCH.buildPath(zone.getId()); + BatchResponse resp = postRequest(endpoint, batchEntry, BatchResponse.class); + checkResponse(resp); + } + private static String buildFqdn(ZoneEntity zone, String sld) { return sld + "." + zone.getName(); } diff --git a/src/main/java/codes/thischwa/cf/CfRequest.java b/src/main/java/codes/thischwa/cf/CfRequest.java index 93c19ef..8a2a626 100644 --- a/src/main/java/codes/thischwa/cf/CfRequest.java +++ b/src/main/java/codes/thischwa/cf/CfRequest.java @@ -43,6 +43,13 @@ public enum CfRequest { * identifier, which need to be provided to construct the complete path. */ RECORD_UPDATE("/zones/%s/dns_records/%s"), + + /** + * Represents the API endpoint path for performing batch operations on DNS records within a specific zone. + * The placeholder "%s" in the path is intended to be replaced by a zone identifier. + * This constant is used to construct the URL for interacting with the batch DNS records API. + */ + RECORD_BATCH("/zones/%s/dns_records/batch"), /** * Represents the API endpoint path for deleting an existing DNS record within a specific DNS * zone. The endpoint path includes placeholders for the zone identifier and the record diff --git a/src/main/java/codes/thischwa/cf/model/BatchEntry.java b/src/main/java/codes/thischwa/cf/model/BatchEntry.java new file mode 100644 index 0000000..bee8682 --- /dev/null +++ b/src/main/java/codes/thischwa/cf/model/BatchEntry.java @@ -0,0 +1,39 @@ +package codes.thischwa.cf.model; + +import java.util.List; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Represents a batch entry containing different types of operations on record entities. + * + *

A BatchEntry groups together collections of operations (patches, posts, puts, and deletes) + * intended to be performed as part of a single batch process. Each operation corresponds to a specific + * type of action on DNS record entities. + * + *

    + *
  • patches: A list of {@link RecordEntity} objects representing partial updates to existing records. + *
  • posts: A list of {@link RecordEntity} objects to be created as new DNS records. + *
  • puts: A list of {@link RecordEntity} objects representing updates or replacements for existing records. + *
  • deletes: A list of {@link RecordEntity} objects with name, type, and content to be removed. + *
+ * + *

This class is used as both a request body for batch operations and to represent the batch response. + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class BatchEntry extends AbstractEntity { + + List patches; + + List posts; + + List puts; + + List deletes; + + @Override + public String getId() { + return ""; + } +} diff --git a/src/main/java/codes/thischwa/cf/model/BatchResponse.java b/src/main/java/codes/thischwa/cf/model/BatchResponse.java new file mode 100644 index 0000000..07fc02c --- /dev/null +++ b/src/main/java/codes/thischwa/cf/model/BatchResponse.java @@ -0,0 +1,8 @@ +package codes.thischwa.cf.model; + +public class BatchResponse extends AbstractSingleResponse { + + BatchResponse() { + super(); + } +} diff --git a/src/main/java/codes/thischwa/cf/model/RecordEntity.java b/src/main/java/codes/thischwa/cf/model/RecordEntity.java index 505e4fe..cc07725 100644 --- a/src/main/java/codes/thischwa/cf/model/RecordEntity.java +++ b/src/main/java/codes/thischwa/cf/model/RecordEntity.java @@ -16,7 +16,7 @@ import org.jetbrains.annotations.Nullable; *

  • Content of the DNS record, such as an IP address. *
  • Flags indicating whether the record is proxiable or proxied. *
  • TTL (Time-To-Live) for the DNS record. - *
  • A locked status to indicate immutability of the record. + *
  • A locked status to indicate the immutability of the record. *
  • Zone-specific metadata including zone ID and name. *
  • Timestamps for creation and modification. * @@ -55,15 +55,35 @@ public class RecordEntity extends AbstractEntity { * @param name the name of the DNS record * @param type the {@link RecordType} of the DNS record * @param ttl the time-to-live (TTL) value for the DNS record - * @param ip the content of the DNS record, typically an IP address + * @param content the content of the DNS record, typically an IP address * @return a {@link RecordEntity} populated with the provided attributes */ - public static RecordEntity build(String name, RecordType type, Integer ttl, String ip) { + public static RecordEntity build(String name, RecordType type, Integer ttl, String content) { RecordEntity rec = new RecordEntity(); rec.setName(name); rec.setType(type.getType()); rec.setTtl(ttl); - rec.setContent(ip); + rec.setContent(content); + return rec; + } + + /** + * Builds and returns a {@link RecordEntity} instance with the specified ID and content. + * + * @param id the unique identifier for the DNS record + * @param content the content of the DNS record, typically an IP address or other record data + * @return a {@link RecordEntity} populated with the provided ID and content + */ + public static RecordEntity build(String id, String content) { + RecordEntity rec = new RecordEntity(); + rec.setId(id); + rec.setContent(content); + return rec; + } + + public static RecordEntity build(String id, String name, String type, Integer ttl, String content) { + RecordEntity rec = build(name, RecordType.valueOf(type), ttl, content); + rec.setId(id); return rec; } } diff --git a/src/main/java/codes/thischwa/cf/model/ResponseResultInfo.java b/src/main/java/codes/thischwa/cf/model/ResponseResultInfo.java index 4db568e..585a9c2 100644 --- a/src/main/java/codes/thischwa/cf/model/ResponseResultInfo.java +++ b/src/main/java/codes/thischwa/cf/model/ResponseResultInfo.java @@ -6,6 +6,7 @@ import lombok.Data; /** * Represents the result of a response with metadata about its success and associated messages or * errors. + * *

    This class provides a structure to capture the outcome of an operation, including: *

      *
    • Whether the operation was successful. @@ -17,6 +18,25 @@ import lombok.Data; @Data public class ResponseResultInfo { private boolean success; - private List errors; + private List errors; private List messages; + + @Data + public static class Error { + private int code; + private String message; + + public Error() { + } + + public Error(int code, String message) { + this.code = code; + this.message = message; + } + + @Override + public String toString() { + return String.format("%d: %s", code, message); + } + } } diff --git a/src/test/java/codes/thischwa/cf/CfClientTest.java b/src/test/java/codes/thischwa/cf/CfClientTest.java index 4909755..da25559 100644 --- a/src/test/java/codes/thischwa/cf/CfClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfClientTest.java @@ -1,17 +1,17 @@ package codes.thischwa.cf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + import codes.thischwa.cf.model.RecordEntity; import codes.thischwa.cf.model.RecordType; import codes.thischwa.cf.model.ZoneEntity; import java.util.List; import java.util.Objects; import lombok.extern.slf4j.Slf4j; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -149,4 +149,48 @@ public class CfClientTest { assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", "")); assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("", "key")); } + + @Test + void testBatch() throws Exception { + // starting point: already existing zone 'mein-d-ns.de' + ZoneEntity z = client.zoneInfo(ZONE_STR); + String sld1 = SLD_STR + "-1"; + String sld2 = SLD_STR + "-2"; + String sld3 = SLD_STR + "-3"; + RecordEntity r1 = RecordEntity.build(sld1, RecordType.A, TTL, "130.0.0.1"); + RecordEntity r2 = RecordEntity.build(sld2, RecordType.A, TTL, "130.0.0.2"); + RecordEntity r3 = RecordEntity.build(sld3, RecordType.A, TTL, "130.0.0.3"); + + // ensure clean state + client.recordDeleteTypeIfExists(z, sld1, RecordType.A); + client.recordDeleteTypeIfExists(z, sld2, RecordType.A); + client.recordDeleteTypeIfExists(z, sld3, RecordType.A); + + try { + // test put + client.recordBatch(z, 30, List.of(r1, r2, r3), null, null); + RecordEntity testRec = client.sldInfo(z, sld1, RecordType.A); + assertEquals("130.0.0.1", testRec.getContent()); + testRec = client.sldInfo(z, sld2, RecordType.A); + assertEquals("130.0.0.2", testRec.getContent()); + testRec = client.sldInfo(z, sld3, RecordType.A); + assertEquals("130.0.0.3", testRec.getContent()); + + // test patch + r1 = client.sldInfo(z, sld1, RecordType.A); + r1.setContent("130.1.0.1"); + client.recordBatch(z, 30, null, List.of(r1), null); + testRec = client.sldInfo(z, sld1, RecordType.A); + assertEquals("130.1.0.1", testRec.getContent()); + + // test delete + client.recordBatch(z, 30, null, null, List.of(r1)); + assertThrows(CloudflareNotFoundException.class, + () -> client.sldInfo(z, sld1, RecordType.A)); + } finally { + client.recordDeleteTypeIfExists(z, sld1, RecordType.A); + client.recordDeleteTypeIfExists(z, sld2, RecordType.A); + client.recordDeleteTypeIfExists(z, sld3, RecordType.A); + } + } } diff --git a/src/test/java/codes/thischwa/cf/ObjectMapperTest.java b/src/test/java/codes/thischwa/cf/ObjectMapperTest.java index ef63f15..203dda6 100644 --- a/src/test/java/codes/thischwa/cf/ObjectMapperTest.java +++ b/src/test/java/codes/thischwa/cf/ObjectMapperTest.java @@ -1,20 +1,53 @@ package codes.thischwa.cf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import codes.thischwa.cf.model.AbstractResponse; +import codes.thischwa.cf.model.BatchResponse; +import codes.thischwa.cf.model.RecordMultipleResponse; +import codes.thischwa.cf.model.RecordSingleResponse; +import codes.thischwa.cf.model.ResponseResultInfo; import codes.thischwa.cf.model.ZoneMultipleResponse; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertNotNull; - +import java.io.InputStream; +import java.util.List; import org.junit.jupiter.api.Test; public class ObjectMapperTest { + private final ObjectMapper mapper = JsonConf.initObjectMapper(); + @Test void testObjectMapper() throws IOException { - ObjectMapper mapper = JsonConf.initObjectMapper(); ZoneMultipleResponse resp = - mapper.readValue(this.getClass().getResourceAsStream("/zone-list-response.json"), - ZoneMultipleResponse.class); + mapper.readValue(this.getClass().getResourceAsStream("/zone-list-response.json"), + ZoneMultipleResponse.class); assertNotNull(resp.getResponseResultInfo()); } + + @Test + void testErrorResponse() throws IOException { + List> respClasses = + List.of(RecordSingleResponse.class, RecordMultipleResponse.class, ZoneMultipleResponse.class, BatchResponse.class); + respClasses.forEach(this::assertErrorResponse); + } + + private void assertErrorResponse(Class clazz) { + InputStream in = this.getClass().getResourceAsStream("/error-response.json"); + try { + AbstractResponse resp = mapper.readValue(in, clazz); + assertNotNull(resp); + assertNotNull(resp.getResponseResultInfo()); + ResponseResultInfo resultInfo = resp.getResponseResultInfo(); + assertFalse(resultInfo.isSuccess()); + assertEquals(1, resultInfo.getErrors().size()); + assertEquals(81053, resultInfo.getErrors().get(0).getCode()); + } catch (IOException e) { + fail("fail for " + clazz + ": " + e.getMessage()); + } + } } diff --git a/src/test/java/codes/thischwa/cf/ResponseValidatorTest.java b/src/test/java/codes/thischwa/cf/ResponseValidatorTest.java index ddd2892..c027dee 100644 --- a/src/test/java/codes/thischwa/cf/ResponseValidatorTest.java +++ b/src/test/java/codes/thischwa/cf/ResponseValidatorTest.java @@ -1,16 +1,16 @@ package codes.thischwa.cf; -import codes.thischwa.cf.model.AbstractResponse; -import codes.thischwa.cf.model.RecordMultipleResponse; -import codes.thischwa.cf.model.ResponseResultInfo; -import codes.thischwa.cf.model.ResultInfo; -import java.util.Arrays; -import lombok.Getter; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; +import codes.thischwa.cf.model.AbstractResponse; +import codes.thischwa.cf.model.RecordMultipleResponse; +import codes.thischwa.cf.model.ResponseResultInfo; +import codes.thischwa.cf.model.ResultInfo; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,9 +48,15 @@ class ResponseValidatorTest { @Test void validateFailedResponse() { + List errors = new ArrayList<>(); + ResponseResultInfo.Error error = new ResponseResultInfo.Error(1, "Fehler 1"); + errors.add(error); + error = new ResponseResultInfo.Error(2, "Fehler 2"); + errors.add(error); + when(mockResponse.getResponseResultInfo()).thenReturn(mockResultInfo); when(mockResultInfo.isSuccess()).thenReturn(false); - when(mockResultInfo.getErrors()).thenReturn(Arrays.asList("Fehler 1", "Fehler 2")); + when(mockResultInfo.getErrors()).thenReturn(errors); CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> validatorWithException.validate(mockResponse, false));