issue #15 Add paging support in recordList API, update tests, and streamline query parameter names.

This commit is contained in:
2026-03-10 14:50:34 +01:00
parent 8d2fc74d04
commit be409139d7
7 changed files with 128 additions and 39 deletions
@@ -116,7 +116,7 @@ public class CfDnsClient extends CfBasicHttpClient {
} }
/** /**
* Provides fluent API access to operations on a specific zone. * Provides fluent API access to operations in a specific zone.
* This method returns a ZoneOperations interface that allows chaining operations * This method returns a ZoneOperations interface that allows chaining operations
* on DNS records within the specified zone. * on DNS records within the specified zone.
* *
@@ -177,6 +177,33 @@ public class CfDnsClient extends CfBasicHttpClient {
return response.getResult().get(0); return response.getResult().get(0);
} }
/**
* Retrieves a list of DNS records for a specified zone, with optional paging support.
*
* @param zone The zone entity containing information about the target zone.
* @return A list of RecordEntity objects representing the DNS records of the specified zone.
* @throws CloudflareApiException If an error occurs during the API request or response processing.
*/
public List<RecordEntity> recordList(ZoneEntity zone) throws CloudflareApiException {
return recordList(zone, (PagingRequest) null);
}
/**
* Retrieves a list of DNS records for a specified zone, with optional paging support.
*
* @param zone The zone entity containing information about the target zone.
* @param pagingRequest The paging request containing parameters such as page size and number.
* @return A list of RecordEntity objects representing the DNS records of the specified zone.
* @throws CloudflareApiException If an error occurs during the API request or response processing.
*/
public List<RecordEntity> recordList(ZoneEntity zone, @Nullable PagingRequest pagingRequest) throws CloudflareApiException {
PagingRequest pr = pagingRequest == null ? PagingRequest.defaultPaging() : pagingRequest;
String endpoint = pr.addQueryString(CfRequest.RECORD_LIST.buildPath(zone.getId()));
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp);
return resp.getResult();
}
/** /**
* Retrieves DNS records for the specified second-level domain (SLD) within a zone. * Retrieves DNS records for the specified second-level domain (SLD) within a zone.
* *
@@ -203,35 +230,17 @@ public class CfDnsClient extends CfBasicHttpClient {
*/ */
public List<RecordEntity> recordList(ZoneEntity zone, String sld, @Nullable RecordType... types) public List<RecordEntity> recordList(ZoneEntity zone, String sld, @Nullable RecordType... types)
throws CloudflareApiException { throws CloudflareApiException {
PagingRequest pagingRequest = PagingRequest.defaultPaging();
List<RecordEntity> recs = recordList(zone, sld, pagingRequest);
return filterAndSetZoneRecords(zone, types, recs);
}
/**
* Retrieves all getRecord entities for a specific second-level domain (SLD) within a given DNS
* zone using the provided paging request parameters.
*
* @param zone The DNS zone entity for which the SLD records are to be fetched.
* @param sld The second-level domain name for which the records are retrieved.
* @param pagingRequest The paging request.
* @return A list of {@code RecordEntity} associated with the desired SLD.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld, PagingRequest pagingRequest)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld); String fqdn = buildFqdn(zone, sld);
String endpoint = String endpoint = CfRequest.RECORD_LIST_NAME.buildPath(zone.getId(), fqdn);
pagingRequest.addQueryString(CfRequest.RECORD_INFO_NAME.buildPath(zone.getId(), fqdn));
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class); RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp); checkResponse(resp, false);
return resp.getResult(); List<RecordEntity> recs = resp.getResult();
return filterAndSetZoneRecords(zone, types, recs);
} }
/** /**
* Retrieves a list of all DNS records for a given zone. * Retrieves a list of all DNS records for a given zone.
* Optionally filters by one or more DNS getRecord types. * Optionally, filters by one or more DNS getRecord types.
* *
* @param zone The zone entity containing information about the domain zone. * @param zone The zone entity containing information about the domain zone.
* @param types Optional parameter specifying one or more DNS getRecord types to filter the results. * @param types Optional parameter specifying one or more DNS getRecord types to filter the results.
@@ -438,7 +447,7 @@ public class CfDnsClient extends CfBasicHttpClient {
Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types)); Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types));
filtered = recs.stream() filtered = recs.stream()
.filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType()))) .filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType())))
.collect(Collectors.toList());; .collect(Collectors.toList());
} else { } else {
filtered = new ArrayList<>(recs); filtered = new ArrayList<>(recs);
} }
@@ -26,25 +26,24 @@ public enum CfRequest {
* needs to be provided to construct the complete path. * needs to be provided to construct the complete path.
*/ */
RECORD_LIST("/zones/%s/dns_records"), RECORD_LIST("/zones/%s/dns_records"),
/**
* Represents the API endpoint path for retrieving information about a DNS getRecord within a
* specific DNS zone by its name. The endpoint path includes placeholders for the zone identifier
* and the getRecord name, which need to be provided to construct the complete path.
*/
RECORD_LIST_NAME("/zones/%s/dns_records?name=%s"),
/** /**
* Represents the API endpoint path for creating a new DNS getRecord within a specific DNS zone. The * Represents the API endpoint path for creating a new DNS getRecord within a specific DNS zone. The
* endpoint path includes a placeholder for the zone identifier, which needs to be provided to * endpoint path includes a placeholder for the zone identifier, which needs to be provided to
* construct the complete path. * construct the complete path.
*/ */
RECORD_CREATE("/zones/%s/dns_records"), RECORD_CREATE("/zones/%s/dns_records"),
/**
* Represents the API endpoint path for retrieving information about a DNS getRecord within a
* specific DNS zone by its name. The endpoint path includes placeholders for the zone identifier
* and the getRecord name, which need to be provided to construct the complete path.
*/
RECORD_INFO_NAME("/zones/%s/dns_records?name=%s"),
/** /**
* Represents the API endpoint path for updating an existing DNS getRecord within a specific DNS * Represents the API endpoint path for updating an existing DNS getRecord within a specific DNS
* zone. The endpoint path includes placeholders for the zone identifier and the getRecord * zone. The endpoint path includes placeholders for the zone identifier and the getRecord
* identifier, which need to be provided to construct the complete path. * identifier, which need to be provided to construct the complete path.
*/ */
RECORD_UPDATE("/zones/%s/dns_records/%s"), RECORD_UPDATE("/zones/%s/dns_records/%s"),
/** /**
* Represents the API endpoint path for performing batch operations on DNS records within a specific zone. * 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. * The placeholder "%s" in the path is intended to be replaced by a zone identifier.
@@ -25,7 +25,7 @@ public class PagingRequest {
* Default page size for retrieving all records in a single request. * Default page size for retrieving all records in a single request.
* Set to a very high value to effectively disable pagination when fetching all records. * Set to a very high value to effectively disable pagination when fetching all records.
*/ */
private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 5_000_000; private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 1000;
private int page; private int page;
private int perPage; private int perPage;
@@ -77,7 +77,7 @@ public class PagingRequest {
} }
private String queryString(boolean add) { private String queryString(boolean add) {
String qs = "page=" + page + "&perPage=" + perPage; String qs = "page=" + page + "&per_page=" + perPage;
return add ? "&" + qs : "?" + qs; return add ? "&" + qs : "?" + qs;
} }
} }
@@ -9,14 +9,18 @@ import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.model.BatchEntry; import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.PagingRequest;
import codes.thischwa.cf.model.RecordEntity; import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType; import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity; import codes.thischwa.cf.model.ZoneEntity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -91,6 +95,19 @@ public class CfClientTest {
} }
} }
@Test
void testZoneList() throws CloudflareApiException {
List<ZoneEntity> zones = client.zoneList();
assertNotNull(zones);
assertFalse(zones.isEmpty());
assertEquals(ZONE_STR, zones.get(0).getName());
zones = client.zoneList(PagingRequest.of(1, 100));
assertNotNull(zones);
assertFalse(zones.isEmpty());
assertEquals(ZONE_STR, zones.get(0).getName());
}
@Test @Test
void testZoneListAnlFailedSldList() throws Exception { void testZoneListAnlFailedSldList() throws Exception {
List<ZoneEntity> zList = client.zoneList(); List<ZoneEntity> zList = client.zoneList();
@@ -445,4 +462,68 @@ public class CfClientTest {
} }
@Test
void testPaging() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
String pagingSld = "paging-" + System.currentTimeMillis();
try {
int existingCount = 0;
try {
List<RecordEntity> allRecords = client.recordList(zone);
existingCount = allRecords.size();
} catch (CloudflareApiException e) {
// ignore
}
// Calculate how many records we need to create to reach at least 12 total A records
// (to test paging with pageSize 5: page 1 = 5, page 2 = 5, page 3 = 2+)
int targetCount = 12;
int recordsToCreate = Math.max(0, targetCount - existingCount);
// Create additional A records if needed
List<RecordEntity> createdRecords = new ArrayList<>();
for (int i = 1; i <= recordsToCreate; i++) {
RecordEntity record = RecordEntity.build(pagingSld, RecordType.A, TTL, "127.0.0." + i);
RecordEntity created = client.recordCreate(zone, record);
createdRecords.add(created);
assertNotNull(created.getId());
}
// Test paging with page size of 5
PagingRequest page1Request = PagingRequest.of(1, 5);
List<RecordEntity> page1Records = client.recordList(zone, page1Request);
assertEquals(5, page1Records.size(), "First page should contain 5 records");
// 2nd page should also contain 5 records (if we have at least 12 total)
PagingRequest page2Request = PagingRequest.of(2, 5);
List<RecordEntity> page2Records = client.recordList(zone, page2Request);
assertEquals(5, page2Records.size(), "Second page should contain at least 5 records");
// 3rd page should contain 2 records
PagingRequest page3Request = PagingRequest.of(3, 5);
List<RecordEntity> page3Records = client.recordList(zone, page3Request);
assertEquals(2, page3Records.size(), "Third page should contain 2 records");
// Verify no overlap between pages
List<String> page1Ids = page1Records.stream().map(RecordEntity::getId).toList();
List<String> page2Ids = page2Records.stream().map(RecordEntity::getId).toList();
List<String> page3Ids = page3Records.stream().map(RecordEntity::getId).toList();
Set<String> generatedRecordIds = new HashSet<>(page1Ids);
generatedRecordIds.addAll(page2Ids);
generatedRecordIds.addAll(page3Ids);
assertEquals(createdRecords.size(), generatedRecordIds.size());
// Verify our created records are in the zone
List<RecordEntity> allRecords = client.recordList(zone);
Set<String> allRecordIds = allRecords.stream().map(RecordEntity::getId).collect(Collectors.toSet());
assertEquals(createdRecords.size(), allRecordIds.size());
assertTrue(allRecordIds.containsAll(generatedRecordIds));
} finally {
try {
client.recordDeleteTypeIfExists(zone, pagingSld, RecordType.A);
} catch (Exception e) { /* ignore */ }
}
}
} }
@@ -27,7 +27,7 @@ public class CfRequestTest {
@Test @Test
public void testBuildRecordInfoName() { public void testBuildRecordInfoName() {
String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sub.domain.com"); String result = CfRequest.RECORD_LIST_NAME.buildPath("zone123", "sub.domain.com");
assertEquals("/zones/zone123/dns_records?name=sub.domain.com", result); assertEquals("/zones/zone123/dns_records?name=sub.domain.com", result);
} }
@@ -45,7 +45,7 @@ public class CfRequestTest {
@Test @Test
public void testBuildRecordInfo() { public void testBuildRecordInfo() {
String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sld.domain.com"); String result = CfRequest.RECORD_LIST_NAME.buildPath("zone123", "sld.domain.com");
assertEquals("/zones/zone123/dns_records?name=sld.domain.com", result); assertEquals("/zones/zone123/dns_records?name=sld.domain.com", result);
} }
@@ -9,13 +9,13 @@ public class PagingRequestTest {
@Test @Test
void testBuildPath() { void testBuildPath() {
String result = PagingRequest.defaultPaging().addQueryString("/zones"); String result = PagingRequest.defaultPaging().addQueryString("/zones");
assertEquals("/zones?page=1&perPage=5000000", result); assertEquals("/zones?page=1&per_page=1000", result);
} }
@Test @Test
void testBuildPathAdditional() { void testBuildPathAdditional() {
String result = new PagingRequest( 10, 100).addQueryString("/zones?foo=bar"); String result = new PagingRequest( 10, 100).addQueryString("/zones?foo=bar");
assertEquals("/zones?foo=bar&page=10&perPage=100", result); assertEquals("/zones?foo=bar&page=10&per_page=100", result);
} }
@Test @Test
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration debug="true">
<appender name="current" <appender name="current"
class="ch.qos.logback.core.ConsoleAppender"> class="ch.qos.logback.core.ConsoleAppender">