48 Commits

Author SHA1 Message Date
thischwa c80cfc8b66 [maven-release-plugin] prepare release v0.2.0 2026-01-19 18:03:46 +01:00
thischwa b07c818127 Update version in pom.xml from 0.2.0-beta-SNAPSHOT to 0.2.0-SNAPSHOT. 2026-01-19 18:02:10 +01:00
thischwa 249dfd2de5 fix #10 - Add lombok-maven-plugin for delomboking, configure maven-jar-plugin to attach delomboked sources, and update pom.xml with necessary properties and dependencies. 2026-01-18 15:22:32 +01:00
thischwa b241d8cf09 issue #9 - Refactor CfClientTest, CfDnsClient, and CfBasicHttpClient: improve error handling, enable record filtering, refine empty result behavior, and update tests for clarity and robustness. 2026-01-11 18:28:22 +01:00
thischwa b88f4f78ba Fix JavaDoc formatting inconsistencies in CfBasicHttpClient, CfDnsClient, and ZoneOperations to improve readability and maintain standard alignment. 2026-01-11 11:46:13 +01:00
thischwa 9e8ee5bb4a Refactor tests: Replace assertTrue(size >= 1) with assertFalse(isEmpty) for clarity and correctness. 2026-01-09 15:25:33 +01:00
thischwa 34010a4e77 issue #9: Add groupRecordsByFqdn utility to CfDnsClient, set zoneId in DNS record methods, and enhance tests for record validation and grouping. 2026-01-07 17:52:48 +01:00
thischwa 0f1248d08b issue #9: Refactor recordGet to recordList for consistency across API methods, update related documentation, and revise impacted tests. 2026-01-06 20:35:50 +01:00
thischwa acf2a2fc3b issue #9: Add recordList method to CfDnsClient, update Zone-level fluent API, and implement tests for DNS record listing with optional type filters. 2026-01-06 20:14:05 +01:00
thischwa 483b79b372 Simplify README code example by removing unnecessary line breaks and improving method chaining clarity. 2026-01-06 15:27:01 +01:00
thischwa 5a6a17798b Simplify README examples by removing unnecessary line breaks, standardizing method chaining, and improving code readability for batch DNS operations and fluent API usage. 2026-01-06 15:20:12 +01:00
thischwa edf1752e81 Simplify README examples by removing unnecessary line breaks, standardizing method chaining, and improving code readability for batch DNS operations and fluent API usage. 2026-01-06 15:07:26 +01:00
thischwa 25fd480e69 Standardize and simplify README code examples by eliminating unnecessary line continuations, improving readability, and enhancing method chaining for DNS operations. 2026-01-06 14:35:25 +01:00
thischwa 5098e17172 Simplify README examples by combining line continuations, improving method chaining clarity, and standardizing formatting for batch DNS operations. 2026-01-06 13:50:30 +01:00
thischwa a221de4792 issue #9:
Change default behavior of `emptyResultThrowsException` to `false`, update `ResponseValidator` for improved handling of single and multiple results, and enhance test coverage and documentation.

Add Fluent API for DNS operations, update `README`, and refactor `CfDnsClient` for zone-level chaining.

Validate record type in `RecordEntity#build`, throw exception for invalid types, and add test coverage.

Refactor `RecordEntity#getSld` to handle `zoneName` cases, improve null safety, and enhance substring logic.

Refactor `RecordEntity` getter (`getName` → `getSld`) for clarity, improve string handling in batch processing, and enhance type safety in HTTP operations. Update tests and documentation accordingly.

Refactor `deleteRequest` and `patchRequest` to accept `responseType` parameter for improved flexibility. Extract methods for cleaning and processing batch DNS records.

Refactor API method names for consistency (`zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` → `recordList`, `sldInfo` → `recordGet`) and update related documentation and tests.
2026-01-06 13:19:19 +01:00
thischwa 1bfea09aa9 Refactor sldInfo to improve JavaDoc, split overloaded methods, and enhance exception handling. 2025-12-31 16:45:59 +01:00
thischwa 12c75914b1 Add test for handling unknown SLDs with sldInfo method. 2025-12-31 16:30:49 +01:00
thischwa dd586be9f4 fix formatting in PagingRequest#getPagingParams JavaDoc and reorder RecordEntity#getName method. 2025-12-31 13:38:32 +01:00
thischwa 77a584afb6 fix issue #8: Refactor sldInfo to support filtering by multiple record types. Adjust related methods, tests, and documentation accordingly. 2025-12-31 13:19:56 +01:00
thischwa 6027e66afe Update changelog: CfClient#sldInfo must return multiple RecordEntries 2025-12-30 15:49:41 +01:00
thischwa 351730dc79 Bump version to 0.2.0-beta-SNAPSHOT and update changelog 2025-12-30 15:46:33 +01:00
thischwa c75f9d74ca fix #7: Refactor sldInfo to return a list of records, update related methods and tests accordingly. Improve logging and exception handling for batch DNS operations. 2025-12-30 15:34:55 +01:00
thischwa 1abe888a65 [maven-release-plugin] prepare for next development iteration 2025-12-30 13:01:18 +01:00
thischwa 80670c2a90 [maven-release-plugin] prepare release v0.2.0-beta.2 2025-12-30 13:01:11 +01:00
thischwa ce1f766326 Add maven-source-plugin to generate and attach source JARs, update changelog 2025-12-30 12:58:07 +01:00
thischwa a965f21d6b Add batch DNS operations documentation and refine logging format in CfDnsClient. Extend tests for record creation and deletion. 2025-12-27 16:48:26 +01:00
thischwa a3dc89e2c5 Update changelog with details for v0.2.0-beta.1 changes 2025-12-26 10:15:47 +01:00
thischwa 9c36fa1988 [maven-release-plugin] prepare for next development iteration 2025-12-26 10:00:45 +01:00
thischwa 335c9a6c5e [maven-release-plugin] prepare release v0.2.0-beta.1 2025-12-26 10:00:40 +01:00
thischwa 9dfb5f4c75 Revert pom.xml version to 0.2.0-beta.1-SNAPSHOT. 2025-12-26 09:57:22 +01:00
thischwa e12eaff137 fixed test for put 2025-12-26 09:47:52 +01:00
thischwa 596006276e fixed test for put 2025-12-25 21:18:30 +01:00
thischwa 43ff58febc Update pom.xml version to 0.2.0-beta.2-SNAPSHOT. 2025-12-25 20:52:03 +01:00
thischwa f2f7f662a1 [maven-release-plugin] prepare for next development iteration 2025-12-25 20:48:03 +01:00
thischwa 9a376415b3 [maven-release-plugin] prepare release v0.2.0-beta.1 2025-12-25 20:47:57 +01:00
thischwa 0eaca55e95 issue #5 - Update pom.xml version to 0.2.0-beta.1-SNAPSHOT, enhance logging, and refine DNS batch record processing method. 2025-12-25 20:43:31 +01:00
thischwa deb6fcf445 Reformat pom.xml for improved readability and consistency. 2025-12-25 16:34:47 +01:00
thischwa e18f8d7017 Update SonarQube organization and project key in pom.xml. 2025-12-25 16:33:53 +01:00
thischwa 4df6757ee4 Update SonarQube project key and organization in GitLab CI; add dynamic project key to analysis command. 2025-12-25 16:26:28 +01:00
thischwa 3d026dab1d Use non-interactive mode (-B) for SonarQube analysis in GitLab CI pipeline. 2025-12-25 16:05:17 +01:00
thischwa c20bb6bb1c Fix GitLab CI SonarQube script by removing unnecessary -B flag. 2025-12-25 16:00:51 +01:00
thischwa a5e96f38f8 Revert "Simplify SonarQube analysis command in GitLab CI pipeline."
This reverts commit 7f56c6f453.
2025-12-25 15:40:33 +01:00
thischwa 4cb9b9929e Revert "Add SonarQube Maven plugin to pom.xml configuration."
This reverts commit 67df00c36e.
2025-12-25 15:40:33 +01:00
thischwa 67df00c36e Add SonarQube Maven plugin to pom.xml configuration. 2025-12-25 15:25:59 +01:00
thischwa 7f56c6f453 Simplify SonarQube analysis command in GitLab CI pipeline. 2025-12-25 15:10:45 +01:00
thischwa a5d7992391 Enhance code readability by improving comments, formatting, and aligning annotations across classes. 2025-12-25 14:43:54 +01:00
thischwa b4ce867efc issue #6 - ResponseResultInfo#Errors: wrong object structure
issue #5 - Add batch DNS record operations and refactor Cloudflare API client.
2025-12-25 14:27:18 +01:00
thischwa eb821a2218 [maven-release-plugin] prepare for next development iteration 2025-08-19 12:30:24 +02:00
29 changed files with 1717 additions and 451 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ variables:
GITLAB_USERNAME: $GITLAB_USERNAME
GITLAB_USEREMAIL: GITLAB_USEREMAIL
SONAR_HOST_URL: $SONAR_HOST_URL
SONAR_PROJECT_KEY: "thischwa_CloudflareDNS-java"
SONAR_ORGANIZATION: "thischwa"
SONAR_PROJECT_KEY: "th-schwarz_CloudflareDNS-java"
SONAR_ORGANIZATION: "th-schwarz"
SONAR_TOKEN: $SONAR_TOKEN
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
@@ -68,7 +68,7 @@ sonarcloud_scan:
dependencies:
- build
script:
- mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
- mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=${SONAR_PROJECT_KEY}
only:
- merge_requests
- develop
+246 -25
View File
@@ -49,6 +49,21 @@ The dependency is:
## Changelog
- 0.2.0-beta-SNAPSHOT:
- **New Fluent API**: Added chainable method interface for more readable DNS operations (
`client.zone().record()...`)
- **Breaking Change**: `emptyResultThrowsException` default changed from `true` to `false`. Now applies to both
single and multiple result requests. Empty results will be returned by default without throwing exceptions.
- API method names refactored for consistency: `zoneListAll``zoneList`, `zoneInfo``zoneGet`, `sldListAll`
`recordList`
- RecordEntity getter methods renamed for clarity: `getName()``getSld()`
- Code quality improvements: eliminated duplication in batch operations, improved type safety in HTTP methods,
optimized string concatenation, removed mutable setters from CfDnsClient
- Enhanced type validation in `RecordEntity.build()` with better error messages
- CfClient#recordList must return multiple RecordEntries
- add a missing source jar
- ResponseResultInfo#Errors: wrong object structure
- changing multiple records with put, post, patch and delete for dns-records
- 0.1.0:
- refactored / extended tests
- 0.1.0-beta.3:
@@ -63,6 +78,11 @@ The methods can be categorized as follows:
- `Zone`: list, info
- `Record`: list, info, create, update, delete
The API provides two styles for working with DNS records:
1. **Traditional API**: Direct method calls with explicit parameters
2. **Fluent API**: Chainable method calls for more readable code
The following text focuses on the basic methods. For further information, take a look at
the [javadoc of the CfDnsClient](https://cloudflaredns-java-f4ee3a.gitlab.io/apidocs/codes/thischwa/cf/CfDnsClient.html).
@@ -74,20 +94,20 @@ CfDnsClient cfDnsClient = new CfDnsClient(
);
```
### `zoneListAll`
### `zoneList`
Retrieve all zones within the Cloudflare account.
- **Returns**: A list of `ZoneEntity` objects.
```java
List<ZoneEntity> zones = cfDnsClient.zoneListAll();
List<ZoneEntity> zones = cfDnsClient.zoneList();
zones.forEach(zone -> System.out.println("Zone: " + zone.getName()));
```
---
### `zoneInfo`
### `zoneGet`
Get detailed information about a specific zone by its name.
@@ -96,14 +116,17 @@ Get detailed information about a specific zone by its name.
- **Returns**: A `ZoneEntity` object.
```java
ZoneEntity zone = cfDnsClient.zoneInfo("example.com");
ZoneEntity zone = cfDnsClient.zoneGet("example.com");
System.out.println("Zone ID: " + zone.getId());
```
---
### `sldListAll`
### `recordList`
Retrieve records for a given zone. There are two variants:
#### List by SLD
Retrieve all records for a specific second-level domain (SLD) under a given zone.
- **Parameters**:
@@ -112,28 +135,35 @@ Retrieve all records for a specific second-level domain (SLD) under a given zone
- **Returns**: A list of `RecordEntity` objects.
```java
List<RecordEntity> records = cfDnsClient.sldListAll(zone, "sld");
List<RecordEntity> records = cfDnsClient.recordList(zone, "sld");
records.forEach(record ->
System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
System.out.println("Record Type: "+record.getType()
+", Value: "+record.getContent())
);
```
---
### `sldInfo`
Retrieve DNS record details for a specific SLD, zone, and record type.
#### List by Zone (optional filtering)
Retrieve DNS record details for a zone or a specific SLD, optionally filtered by record types.
- **Parameters**:
- `ZoneEntity zone` - The zone object.
- `String sld` - The second-level domain.
- `RecordType type` - Record type (e.g., A, CNAME).
- `String sld` - (Optional) The second-level domain.
- `RecordType... types` - Optional record types to filter by (e.g., A, CNAME). If not specified, returns all record types.
- **Returns**: A list of `RecordEntity` objects matching the criteria.
```java
RecordEntity record = cfDnsClient.sldInfo(zone, "www", RecordType.A);
System.out.println("Record IP: " + record.getContent());
```
// Get all records for a specific SLD
List<RecordEntity> allSldRecords = cfDnsClient.recordList(zone, "www");
// Get only A records for a specific SLD
List<RecordEntity> aSldRecords = cfDnsClient.recordList(zone, "www", RecordType.A);
// Get all records of a zone
List<RecordEntity> allZoneRecords = cfDnsClient.recordList(zone);
// Get only A and AAAA records of a zone
List<RecordEntity> ipZoneRecords = cfDnsClient.recordList(zone, RecordType.A, RecordType.AAAA);
```
---
### `recordCreate`
@@ -197,20 +227,212 @@ cfDnsClient.recordDeleteTypeIfExists(zone, "api", RecordType.A);
System.out.println("Deletion attempt completed.");
```
### Batch Operations
Process multiple DNS record operations (POST, PUT, PATCH, DELETE) in a single batch request.
- **Parameters**:
- `ZoneEntity zone` - The target zone.
- `List<RecordEntity> postRecords` - Records to create (nullable). Can be built without IDs.
- `List<RecordEntity> putRecords` - Records to fully replace (nullable). **Requires record IDs**.
- `List<RecordEntity> patchRecords` - Records to partially update (nullable). **Requires record IDs**.
- `List<RecordEntity> deleteRecords` - Records to delete (nullable). **Requires record IDs**.
- **Returns**: A `BatchEntry` object containing the processed records.
**Important**: For UPDATE (PATCH), REPLACE (PUT), and DELETE operations, you must first retrieve the existing records to obtain their IDs.
#### Batch Create (POST)
Create new records, IDs are not required:
```java
List<RecordEntity> newRecords = Arrays.asList(
RecordEntity.build("cdn." + zone.getName(), RecordType.A, 60, "192.168.1.11"),
RecordEntity.build("mail." + zone.getName(), RecordType.A, 60, "192.168.1.12")
);
BatchEntry result = cfDnsClient.recordBatch(zone, newRecords, null, null, null);
System.out.println("Created "+result.getPosts().size() +" records.");
```
#### Batch Update (PATCH)
Partially update existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToUpdate = new ArrayList<>();
recordsToUpdate.add(cfDnsClient.recordList(zone, "api",RecordType.A).get(0));
recordsToUpdate.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
// Step 2: Modify only the fields you want to update
recordsToUpdate.forEach(record ->record.setContent("192.168.2.10"));
// Step 3: Send batch update request
BatchEntry result = cfDnsClient.recordBatch(zone, null, null, recordsToUpdate, null);
System.out.println("Updated "+result.getPatches().size() +" records.");
```
#### Batch Replace (PUT)
Fully replace existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToReplace = new ArrayList<>();
recordsToReplace.add(cfDnsClient.recordList(zone, "mail",RecordType.A).get(0));
recordsToReplace.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
// Step 2: Modify all fields as needed (full replacement)
recordsToReplace.get(0).setContent("192.168.3.10");
recordsToReplace.get(0).setTtl(120);
recordsToReplace.get(1).setContent("192.168.3.11");
recordsToReplace.get(1).setTtl(120);
// Step 3: Send batch replace request
BatchEntry result = cfDnsClient.recordBatch(zone, null, recordsToReplace, null, null);
System.out.println("Replaced "+result.getPuts().size() +" records.");
```
#### Batch Delete
Delete existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToDelete = new ArrayList<>();
recordsToDelete.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
recordsToDelete.add(cfDnsClient.recordList(zone, "mail",RecordType.A).get(0));
// Step 2: Send batch delete request
BatchEntry result = cfDnsClient.recordBatch(zone, null, null, null, recordsToDelete);
System.out.println("Deleted "+recordsToDelete.size() +" records.");
```
#### Combined Batch Operations
Combine multiple operations in a single batch request:
```java
// Create new records (no IDs needed)
List<RecordEntity> newRecords = Arrays.asList(
RecordEntity.build("new-api." + zone.getName(), RecordType.A, 60, "192.168.1.100")
);
// Fetch existing records for update/delete (IDs required)
List<RecordEntity> recordsToUpdate = Arrays.asList(
cfDnsClient.recordList(zone, "existing-api", RecordType.A).get(0)
);
recordsToUpdate.get(0).setContent("192.168.1.200");
List<RecordEntity> recordsToDelete = Arrays.asList(
cfDnsClient.recordList(zone, "old-api", RecordType.A).get(0)
);
// Execute all operations in a single batch request
BatchEntry result = cfDnsClient.recordBatch(
zone,
newRecords, // POST - create new records
null, // PUT - not used in this example
recordsToUpdate, // PATCH - update existing records
recordsToDelete // DELETE - remove records
);
System.out.println("Batch completed:");
System.out.println(" Created: "+result.getPosts().size());
System.out.println(" Updated: "+result.getPatches().size());
System.out.println(" Deleted: "+result.getDeletes().size());
```
---
### Notes on Error Handling
## Fluent API
The fluent API provides a chainable, readable interface for DNS operations. It's an alternative to the traditional API
that reduces verbosity and improves code readability.
### Basic Usage
```java
// Create a DNS record
client.zone("example.com")
.record("api")
.create(RecordType.A, "192.168.1.1",60);
// Get DNS records of a subdomain
List<RecordEntity> records = client.zone("example.com")
.record("www", RecordType.A)
.get();
// Get all DNS records of a zone
List<RecordEntity> zoneRecords = client.zone("example.com")
.list(RecordType.A, RecordType.AAAA);
// Update a DNS record
RecordEntity updated = client.zone("example.com")
.record("api", RecordType.A)
.update("192.168.1.2");
// Delete DNS records
client.zone("example.com")
.record("old-service")
.delete(RecordType.A, RecordType.AAAA);
```
### Advantages of Fluent API
- **More Readable**: The chain of method calls reads like natural language
- **Less Verbose**: No need to pass zone objects between method calls
- **Type Safe**: Full compile-time type checking
- **IDE Friendly**: Excellent autocomplete support
### Complete Example
```java
CfDnsClient client = new CfDnsClient("email@example.com", "yourApiKey");
// Create a new record
client.zone("example.com")
.record("api")
.create(RecordType.A, "192.168.100.1",60);
// Retrieve and verify
List<RecordEntity> records = client.zone("example.com")
.record("api", RecordType.A)
.get();
System.out.println("IP: "+records.get(0).getContent());
// Update the record
client.zone("example.com")
.record("api",RecordType.A)
.update("192.168.100.2");
// Clean up
client.zone("example.com")
.record("api")
.delete(RecordType.A);
```
---
# Notes on Error Handling
The `CfDnsClient` provides internal error-handling mechanisms through exceptions. For example:
- `CloudflareApiException` is thrown for errors during API communication or invalid responses.
- `CloudflareNotFoundException` is thrown when the requested single resource is not found, if enabled via the `emptyResultThrowsException` flag during initialization.
- `CloudflareNotFoundException` is thrown when the requested resource (single or multiple) is not found, if enabled via the `emptyResultThrowsException` flag during initialization. **Default is `false`**, meaning empty results will be returned without throwing an exception.
#### Example:
To enable exception throwing for empty results:
```java
CfDnsClient client = new CfDnsClient(true, "email@example.com", "yourApiKey");
```
## Example:
```java
try {
RecordEntity record = cfDnsClient.sldInfo(zone, "www", RecordType.A);
System.out.println("Record IP: " + record.getContent());
List<RecordEntity> records = cfDnsClient.recordList(zone, "www", RecordType.A);
System.out.println("Record IP: "+records.get(0).getContent());
} catch (CloudflareApiException e) {
if (e instanceof CloudflareNotFoundException) {
log.warn("Sld not found: www");
@@ -223,7 +445,6 @@ try {
---
### Summary
# Summary
`CfDnsClient` offers a simple interface for managing DNS entries via Cloudflare's public API, allowing seamless CRUD
operations and automation-friendly workflows.
`CfDnsClient` offers a simple interface for managing DNS entries via Cloudflare's public API, allowing seamless CRUD operations and automation-friendly workflows.
+52 -4
View File
@@ -4,7 +4,7 @@
<groupId>codes.thischwa</groupId>
<artifactId>cloudflaredns</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
<name>CloudflareDNS-java</name>
<inceptionYear>2025</inceptionYear>
<packaging>jar</packaging>
@@ -41,20 +41,22 @@
<mockito-junit5.version>5.17.0</mockito-junit5.version>
<!-- sonarqube -->
<sonar.organization>thischwa</sonar.organization>
<sonar.organization>th-schwarz</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
<sonar.sourceEncoding>${file.encoding}</sonar.sourceEncoding>
<sonar.projectKey>thischwa_CloudflareDNS-java</sonar.projectKey>
<sonar.projectKey>th-schwarz_CloudflareDNS-java</sonar.projectKey>
<sonar.projectName>CloudflareDNS-java</sonar.projectName>
<sonar.branch.name>develop</sonar.branch.name>
<sonar.test.exclusions>src/test/java/**/*</sonar.test.exclusions>
<lombok-maven-plugin.version>1.18.20.0</lombok-maven-plugin.version>
</properties>
<scm>
<developerConnection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</developerConnection>
<connection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</connection>
<url>https://gitlab.com/th-schwarz/CloudflareDNS-java</url>
<tag>v0.1.0</tag>
<tag>v0.2.0</tag>
</scm>
<distributionManagement>
@@ -228,6 +230,52 @@
<version>3.5.3</version>
</plugin>
<plugin>
<!-- delombok -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>${lombok-maven-plugin.version}</version>
<executions>
<execution>
<id>delombok-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDirectory>src/main/java</sourceDirectory>
<outputDirectory>${project.build.directory}/delombok</outputDirectory>
<addOutputDirectory>false</addOutputDirectory>
</configuration>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>attach-delomboked-sources</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classesDirectory>${project.build.directory}/delombok</classesDirectory>
<classifier>sources</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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,25 @@ abstract class CfBasicHttpClient {
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
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.");
StringBuilder errorMessage = new StringBuilder("API error");
if (!respObj.getResponseResultInfo().getErrors().isEmpty()) {
errorMessage.append(": ");
respObj.getResponseResultInfo().getErrors().forEach(e -> {
log.error(" - {}", e.toString());
errorMessage.append(e).append("; ");
});
// Remove trailing "; "
if (errorMessage.toString().endsWith("; ")) {
errorMessage.setLength(errorMessage.length() - 2);
}
}
throw new CloudflareApiException(errorMessage.toString());
}
logUri = request.getRequestUri();
if (result.statusCode >= 200 && result.statusCode < 300) {
return respObj;
} else {
log.error("{} request failed for URL {}: Status {}", request.getMethod(), request.getUri(),
@@ -104,28 +113,36 @@ abstract class CfBasicHttpClient {
/**
* Sends a DELETE request to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T deleteRequest(String endpoint) throws CloudflareApiException {
<T extends AbstractResponse> T deleteRequest(String endpoint, Class<T> responseType) throws CloudflareApiException {
HttpDelete request = new HttpDelete(buildUrl(endpoint));
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
return executeRequest(request, responseType);
}
/**
* Sends a POST request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse, R extends AbstractEntity> T postRequest(String endpoint,
R requestPayload)
<T extends AbstractResponse> T postRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPost request = new HttpPost(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, (Class<T>) 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 extends AbstractResponse, R extends AbstractEntity> T putRequest(String endpoint,
R requestPayload,
<T extends AbstractResponse> T putRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPut request = new HttpPut(buildUrl(endpoint));
@@ -135,23 +152,33 @@ abstract class CfBasicHttpClient {
/**
* Sends a PATCH request with a payload to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param requestPayload the payload to send
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse, R extends AbstractEntity> T patchRequest(String endpoint,
R requestPayload)
<T extends AbstractResponse> T patchRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPatch request = new HttpPatch(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
return executeRequest(request, responseType);
}
/**
* Sets the JSON payload for a request.
*/
private <R extends AbstractEntity> 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.trace("Request methode [{}] payload: {}", request.getMethod(), jsonPayload);
request.setEntity(new StringEntity(jsonPayload,
ContentType.APPLICATION_JSON));
} catch (JsonProcessingException e) {
throw new CloudflareApiException("Error serializing JSON payload", e);
+216 -52
View File
@@ -1,6 +1,10 @@
package codes.thischwa.cf;
import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.fluent.ZoneOperationsImpl;
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 +12,14 @@ 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.Arrays;
import java.util.HashMap;
import java.util.List;
import lombok.Setter;
import java.util.Map;
import java.util.stream.Collectors;
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
@@ -25,25 +34,26 @@ import lombok.extern.slf4j.Slf4j;
* "yourApiKey"
* );
* // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneInfo("example.com");
* ZoneEntity zone = cfDnsClient.zoneGet("example.com");
* System.out.println("Zone ID: " + zone.getId());
* // Retrieve records of a subdomain
* List&lt;{@link RecordEntity}&gt; records = cfDnsClient.sldListAll(zone, "sld");
* List&lt;{@link RecordEntity}&gt; records = cfDnsClient.recordGet(zone, "sld");
* records.forEach(record ->
* System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
* );
* // Create a record for the subdomain "api"
* RecordEntity created = client.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.10);
* RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10");
* System.out.println("Created Record ID: " + created.getId());
* </code></pre>
*/
@Setter
@Slf4j
public class CfDnsClient extends CfBasicHttpClient {
private static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
private final ResponseValidator responseValidator;
private final boolean emptyResultThrowsException;
/**
* Constructs a new instance of {@code CfDnsClient}.
*
@@ -66,15 +76,15 @@ public class CfDnsClient extends CfBasicHttpClient {
* process.
*/
public CfDnsClient(String baseUrl, String authEmail, String authKey) {
this(true, baseUrl, authEmail, authKey);
this(false, baseUrl, authEmail, authKey);
}
/**
* Constructs a new instance of {@code CfDnsClient}.
*
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty, it's valid for 'list
* requests' only. Default is true.
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param authEmail The email address associated with the Cloudflare account,
* used for authentication.
* @param authKey The API key of the Cloudflare account, used as part of the
@@ -88,8 +98,8 @@ public class CfDnsClient extends CfBasicHttpClient {
* Constructs a new instance of {@code CfDnsClient}.
*
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty, it's valid for 'list
* requests' only. Default is true.
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param baseUrl The base URL for the Cloudflare API endpoint.
* @param authEmail The email associated with the Cloudflare account for
* authentication.
@@ -100,6 +110,47 @@ public class CfDnsClient extends CfBasicHttpClient {
String authKey) {
super(baseUrl, authEmail, authKey);
this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException;
}
private static String buildFqdn(ZoneEntity zone, String sld) {
return sld + "." + zone.getName();
}
/**
* Groups a list of DNS records by their fully qualified domain name (FQDN).
*
* @param records A list of {@link RecordEntity} objects to be grouped by FQDN.
* @return A map where the key is the FQDN (name field) and the value is a list of {@link RecordEntity}
* objects that share that FQDN.
*/
public static Map<String, List<RecordEntity>> groupRecordsByFqdn(List<RecordEntity> records) {
if (records == null) {
return new HashMap<>();
}
return records.stream()
.collect(Collectors.groupingBy(RecordEntity::getName));
}
/**
* Provides fluent API access to operations on a specific zone.
* This method returns a ZoneOperations interface that allows chaining operations
* on DNS records within the specified zone.
*
* <p>Example:
* <pre><code>
* client.zone("example.com")
* .record("api")
* .create(RecordType.A, "192.168.1.1", 60);
* </code></pre>
*
* @param zoneName the name of the DNS zone (e.g., "example.com")
* @return a ZoneOperations instance for chaining operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
public ZoneOperations zone(String zoneName) throws CloudflareApiException {
ZoneEntity zoneEntity = zoneGet(zoneName);
return new ZoneOperationsImpl(this, zoneEntity);
}
/**
@@ -108,8 +159,8 @@ public class CfDnsClient extends CfBasicHttpClient {
* @return A list of ZoneEntity objects representing the zones retrieved from the Cloudflare API.
* @throws CloudflareApiException If an error occurs during the API request or response handling.
*/
public List<ZoneEntity> zoneListAll() throws CloudflareApiException {
return zoneListAll(PagingRequest.defaultPaging());
public List<ZoneEntity> zoneList() throws CloudflareApiException {
return zoneList(PagingRequest.defaultPaging());
}
/**
@@ -121,7 +172,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @throws CloudflareApiException if there is an error during the API request or response
* processing
*/
public List<ZoneEntity> zoneListAll(PagingRequest pagingRequest) throws CloudflareApiException {
public List<ZoneEntity> zoneList(PagingRequest pagingRequest) throws CloudflareApiException {
String endpoint = pagingRequest.addQueryString(CfRequest.ZONE_LIST.buildPath());
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response);
@@ -136,7 +187,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @throws CloudflareApiException If an error occurs while making the API request or processing
* the response.
*/
public ZoneEntity zoneInfo(String name) throws CloudflareApiException {
public ZoneEntity zoneGet(String name) throws CloudflareApiException {
String endpoint = CfRequest.ZONE_INFO.buildPath(name);
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response, true);
@@ -144,21 +195,40 @@ public class CfDnsClient extends CfBasicHttpClient {
}
/**
* Retrieves all record entities for a specific second-level domain (SLD) within a given DNS
* zone.
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
*
* @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.
* @return A list of {@code RecordEntity} associated with the desired SLD.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
* @param zone the zone entity representing the DNS zone to query
* @param sld the second-level domain (SLD) to filter the records
* @return a list of RecordEntity objects that match the specified SLD within the zone
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> sldListAll(ZoneEntity zone, String sld) throws CloudflareApiException {
return sldListAll(zone, sld, PagingRequest.defaultPaging());
public List<RecordEntity> recordList(ZoneEntity zone, String sld) throws CloudflareApiException {
return recordList(zone, sld, (RecordType[]) null);
}
/**
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
* Optionally filters by one or more DNS record types.
*
* @param zone The zone entity containing information about the domain zone.
* @param sld The second-level domain (SLD) for which to retrieve DNS records.
* @param types Optional parameter specifying one or more DNS record types to filter the results.
* @return A list of {@code RecordEntity} objects representing the DNS records for the specified domain.
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld, @Nullable RecordType... types)
throws CloudflareApiException {
PagingRequest pagingRequest = PagingRequest.defaultPaging();
List<RecordEntity> recs = recordList(zone, sld, pagingRequest);
return filterAndSetZoneRecords(zone, types, recs);
}
/**
* Retrieves all record entities for a specific second-level domain (SLD) within a given DNS
* zone.
* 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.
@@ -166,7 +236,7 @@ public class CfDnsClient extends CfBasicHttpClient {
* @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> sldListAll(ZoneEntity zone, String sld, PagingRequest pagingRequest)
public List<RecordEntity> recordList(ZoneEntity zone, String sld, PagingRequest pagingRequest)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
String endpoint =
@@ -177,22 +247,21 @@ public class CfDnsClient extends CfBasicHttpClient {
}
/**
* Retrieves detailed information about a specific second-level domain (SLD) record for a given
* zone and record type from the Cloudflare API.
* Retrieves a list of all DNS records for a given zone.
* Optionally filters by one or more DNS record types.
*
* @param zone the zone entity that contains information about the DNS zone
* @param sld the second-level domain (SLD) for which the record information is requested
* @param type the type of DNS record (e.g., A, AAAA, CNAME) being queried
* @return the {@link RecordEntity} of the requested SLD and record type
* @throws CloudflareApiException if an error occurs during interaction with the Cloudflare API
* @param zone The zone entity containing information about the domain zone.
* @param types Optional parameter specifying one or more DNS record types to filter the results.
* @return A list of {@code RecordEntity} objects representing the DNS records for the specified zone.
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public RecordEntity sldInfo(ZoneEntity zone, String sld, RecordType type)
public List<RecordEntity> recordList(ZoneEntity zone, RecordType... types)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
String endpoint = CfRequest.RECORD_INFO_NAME_TYPE.buildPath(zone.getId(), fqdn, type);
String endpoint = CfRequest.RECORD_LIST.buildPath(zone.getId());
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp, true);
return resp.getResult().get(0);
checkResponse(resp, false);
List<RecordEntity> recs = resp.getResult();
return filterAndSetZoneRecords(zone, types, recs);
}
/**
@@ -244,10 +313,12 @@ 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();
log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType());
RecordEntity record = resp.getResult();
record.setZoneId(zone.getId());
return record;
}
/**
@@ -262,9 +333,9 @@ 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.getSld(), rec.getType());
} else {
log.warn("Record {} of the type {} was not deleted.", rec.getName(), rec.getType());
log.warn("Record {} of the type [{}] was not deleted.", rec.getSld(), rec.getType());
}
return changed;
}
@@ -280,9 +351,9 @@ public class CfDnsClient extends CfBasicHttpClient {
*/
public boolean recordDelete(ZoneEntity zone, String id) throws CloudflareApiException {
String endpoint = CfRequest.RECORD_DELETE.buildPath(zone.getId(), id);
RecordSingleResponse resp = deleteRequest(endpoint);
RecordSingleResponse resp = deleteRequest(endpoint, RecordSingleResponse.class);
checkResponse(resp);
log.debug("Record {} successful deleted.", id);
log.debug("Record id#{} successful deleted.", id);
return resp.getResult().getId().equals(id);
}
@@ -301,9 +372,9 @@ public class CfDnsClient extends CfBasicHttpClient {
rec.setModifiedOn(null);
rec.setCreatedOn(null);
String endpoint = CfRequest.RECORD_UPDATE.buildPath(zone.getId(), rec.getId());
RecordSingleResponse resp = patchRequest(endpoint, rec);
RecordSingleResponse resp = patchRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful updated.", rec.getName(), rec.getType());
log.info("Record {} of type {} successful updated.", rec.getSld(), rec.getType());
return resp.getResult();
}
@@ -313,25 +384,118 @@ public class CfDnsClient extends CfBasicHttpClient {
*
* @param zone The DNS zone entity in which the record exists.
* @param sld The second-level domain for which the record is being checked.
* @param recordTypes The types of DNS records that should be deleted, if they exist.
* @param recordTypes The types of DNS records that should be deleted if they exist.
* @throws CloudflareApiException If an error occurs during API communication.
*/
public void recordDeleteTypeIfExists(ZoneEntity zone, String sld, RecordType... recordTypes)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
for (RecordType recordType : recordTypes) {
List<RecordEntity> recs;
try {
recs = recordList(zone, sld, recordTypes);
} catch (CloudflareNotFoundException e) {
log.trace("No record of type {} found for domain {}.", recordTypes, fqdn);
return;
}
for (RecordEntity rec : recs) {
try {
RecordEntity rec = sldInfo(zone, sld, recordType);
recordDelete(zone, rec);
log.info("Record {} of type {} successful deleted.", fqdn, recordTypes);
} catch (CloudflareNotFoundException e) {
log.debug("Record {} of type {} does not exist.", fqdn, recordTypes);
} catch (CloudflareApiException e) {
log.error("Failed to delete record {} of type {} for zone {}: {}", fqdn, recordTypes, zone.getName(), e.getMessage());
}
}
}
private static String buildFqdn(ZoneEntity zone, String sld) {
return sld + "." + zone.getName();
/**
* Processes a batch of DNS record operations (POST, PUT, PATCH, DELETE) for a specified zone.
* This method builds and cleans the input records, sends the batch request to the Cloudflare API,
* and returns a result containing processed batch entries.
*
* @param zone The zone entity to which the records belong.
* @param postRecords A list of DNS records to be created (POST). This parameter is nullable.
* @param putRecords A list of DNS records to be fully replaced (PUT). This parameter is nullable.
* @param patchRecords A list of DNS records to be partially updated (PATCH). This parameter is nullable.
* @param deleteRecords A list of DNS records to be deleted (DELETE). This parameter is nullable.
* @return The resulting {@link BatchEntry} containing the processed records after the batch operation.
* @throws CloudflareApiException If an error occurs while communicating with the Cloudflare API.
*/
public BatchEntry recordBatch(ZoneEntity zone, @Nullable List<RecordEntity> postRecords, @Nullable List<RecordEntity> putRecords,
@Nullable List<RecordEntity> patchRecords, @Nullable List<RecordEntity> deleteRecords)
throws CloudflareApiException {
BatchEntry batchEntry = new BatchEntry();
// build 'clean' record entries
if (postRecords != null) {
batchEntry.setPosts(cleanRecordsForPostOrPut(postRecords));
}
if (putRecords != null) {
batchEntry.setPuts(cleanRecordsForPostOrPut(putRecords));
}
if (patchRecords != null) {
batchEntry.setPatches(cleanRecordsForPatch(patchRecords));
}
if (deleteRecords != null) {
batchEntry.setDeletes(cleanRecordsForDelete(deleteRecords));
}
String endpoint = CfRequest.RECORD_BATCH.buildPath(zone.getId());
BatchResponse resp = postRequest(endpoint, batchEntry, BatchResponse.class);
checkResponse(resp);
// set zone id
BatchEntry result = resp.getResult();
setZoneIdForBatchResults(result, zone.getId());
return result;
}
private List<RecordEntity> filterAndSetZoneRecords(ZoneEntity zone, @Nullable RecordType[] types, List<RecordEntity> recs)
throws CloudflareNotFoundException {
List<RecordEntity> filtered;
if (types != null && types.length > 0) {
filtered = recs.stream().filter(rec -> Arrays.asList(types).contains(RecordType.valueOf(rec.getType()))).collect(Collectors.toList());
} else {
filtered = new ArrayList<>(recs);
}
filtered.forEach(rec -> rec.setZoneId(zone.getId()));
// special exception for an empty result, normally it's done in the RecordValidator
if (filtered.isEmpty() && emptyResultThrowsException) {
throw new CloudflareNotFoundException("No records exist after filtering zone: " + zone.getName());
}
return filtered;
}
private List<RecordEntity> cleanRecordsForPostOrPut(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), rec.getTtl(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForPatch(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForDelete(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), null, rec.getContent())));
return cleaned;
}
private void setZoneIdForBatchResults(BatchEntry result, String zoneId) {
if (result.getPosts() != null) {
result.getPosts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPuts() != null) {
result.getPuts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPatches() != null) {
result.getPatches().forEach(rec -> rec.setZoneId(zoneId));
}
}
private void checkResponse(AbstractResponse resp) throws CloudflareApiException {
+16 -8
View File
@@ -9,7 +9,9 @@ import lombok.Getter;
@Getter
public enum CfRequest {
/** Represents the API endpoint path for retrieving the list of DNS zones. */
/**
* Represents the API endpoint path for retrieving the list of DNS zones.
*/
ZONE_LIST("/zones"),
/**
* Represents the API endpoint path for retrieving information about a specific DNS zone by its
@@ -18,6 +20,12 @@ public enum CfRequest {
*/
ZONE_INFO("/zones?name=%s"),
/**
* Represents the API endpoint path for retrieving information about DNS records within a
* specific DNS zone. The endpoint path includes a placeholder for the zone identifier, which
* needs to be provided to construct the complete path.
*/
RECORD_LIST("/zones/%s/dns_records"),
/**
* Represents the API endpoint path for creating a new DNS record within a specific DNS zone. The
* endpoint path includes a placeholder for the zone identifier, which needs to be provided to
@@ -30,19 +38,19 @@ public enum CfRequest {
* and the record 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 retrieving information about a DNS record within a
* specific DNS zone by its name and type. The endpoint path includes placeholders for the zone
* identifier, record name, and record type, which need to be provided to construct the complete
* path.
*/
RECORD_INFO_NAME_TYPE("/zones/%s/dns_records?name=%s&type=%s"),
/**
* Represents the API endpoint path for updating an existing DNS record within a specific DNS
* zone. The endpoint path includes placeholders for the zone identifier and the record
* 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
@@ -7,7 +7,8 @@ import java.io.Serial;
*/
public class CloudflareApiException extends Exception {
@Serial private static final long serialVersionUID = 1L;
@Serial
private static final long serialVersionUID = 1L;
/**
* Constructs a new CloudflareApiException with the specified detail message.
@@ -1,6 +1,7 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.AbstractSingleResponse;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import java.util.stream.Collectors;
@@ -14,9 +15,11 @@ import java.util.stream.Collectors;
* <li>It checks whether the API response was successful by analyzing the associated response
* metadata. If the response indicates failure, an exception is thrown with descriptive error
* messages.
* <li>If a {@link RecordMultipleResponse} is used, it validates the number of results in the API
* response payload to detect unexpected counts. Depending on the parameter
* 'emptyResultThrowsException', an exception will be triggered or an empty result will be returned.
* <li>It validates the number of results in the API response payload to detect unexpected counts.
* For {@link RecordMultipleResponse}, it checks if results are empty or if more than one result
* was returned when a single result was expected. For {@link AbstractSingleResponse}, it checks
* if the result is null. Depending on the parameter 'emptyResultThrowsException', an exception
* will be triggered or an empty/null result will be returned.
* </ul>
*/
class ResponseValidator {
@@ -50,6 +53,10 @@ class ResponseValidator {
if (emptyResultThrowsException && respMulti.getResultInfo().totalCount() == 0) {
throw new CloudflareNotFoundException("No result found");
}
} else if (resp instanceof AbstractSingleResponse<?> respSingle) {
if (emptyResultThrowsException && respSingle.getResult() == null) {
throw new CloudflareNotFoundException("No result found");
}
}
}
@@ -0,0 +1,49 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import java.util.List;
/**
* Fluent interface for record-level operations.
* Provides a chainable API for CRUD operations on DNS records.
*/
public interface RecordOperations {
/**
* Retrieves DNS records for the selected subdomain.
*
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
List<RecordEntity> get() throws CloudflareApiException;
/**
* Creates a new DNS record with the specified parameters.
*
* @param type the DNS record type (e.g., A, AAAA, CNAME)
* @param content the content of the DNS record (e.g., IP address)
* @param ttl the time-to-live value in seconds
* @return the created RecordEntity
* @throws CloudflareApiException if an error occurs while creating the record
*/
RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException;
/**
* Updates an existing DNS record with new content.
*
* @param newContent the new content for the DNS record
* @return the updated RecordEntity
* @throws CloudflareApiException if an error occurs while updating the record
*/
RecordEntity update(String newContent) throws CloudflareApiException;
/**
* Deletes DNS records of the specified types.
*
* @param types the DNS record types to delete
* @throws CloudflareApiException if an error occurs while deleting records
*/
void delete(RecordType... types) throws CloudflareApiException;
}
@@ -0,0 +1,64 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of RecordOperations for fluent API access to record-level operations.
*/
public class RecordOperationsImpl implements RecordOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
private final String sld;
private final RecordType[] types;
/**
* Constructs a RecordOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
* @param sld the subdomain (second-level domain) name
* @param types optional array of RecordType to filter by
*/
public RecordOperationsImpl(CfDnsClient client, ZoneEntity zone, String sld, @Nullable RecordType[] types) {
this.client = client;
this.zone = zone;
this.sld = sld;
this.types = types;
}
@Override
public List<RecordEntity> get() throws CloudflareApiException {
return client.recordList(zone, sld, types);
}
@Override
public RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException {
return client.recordCreateSld(zone, sld, ttl, type, content);
}
@Override
public RecordEntity update(String newContent) throws CloudflareApiException {
List<RecordEntity> records = get();
if (records.isEmpty()) {
throw new CloudflareApiException("No records found to update for subdomain: " + sld);
}
if (records.size() > 1) {
throw new CloudflareApiException("Multiple records found. Please use recordUpdate() directly for precise control.");
}
RecordEntity record = records.get(0);
record.setContent(newContent);
return client.recordUpdate(zone, record);
}
@Override
public void delete(RecordType... types) throws CloudflareApiException {
client.recordDeleteTypeIfExists(zone, sld, types);
}
}
@@ -0,0 +1,40 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordType;
import org.jetbrains.annotations.Nullable;
/**
* Fluent interface for zone-level operations.
* Provides a chainable API for accessing and manipulating DNS records within a specific zone.
*/
public interface ZoneOperations {
/**
* Selects a record (subdomain) within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations record(String sld) throws CloudflareApiException;
/**
* Selects a record with specific types within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @param types optional DNS record types to filter by
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException;
/**
* Lists all DNS records within the zone, optionally filtered by types.
*
* @param types optional DNS record types to filter by
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
java.util.List<codes.thischwa.cf.model.RecordEntity> list(@Nullable RecordType... types) throws CloudflareApiException;
}
@@ -0,0 +1,44 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of ZoneOperations for fluent API access to zone-level operations.
*/
public class ZoneOperationsImpl implements ZoneOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
/**
* Constructs a ZoneOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
*/
public ZoneOperationsImpl(CfDnsClient client, ZoneEntity zone) {
this.client = client;
this.zone = zone;
}
@Override
public RecordOperations record(String sld) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, null);
}
@Override
public RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, types);
}
@Override
public List<RecordEntity> list(@Nullable RecordType... types) throws CloudflareApiException {
return client.recordList(zone, types);
}
}
@@ -0,0 +1,31 @@
/**
* Fluent API interfaces and implementations for chainable DNS operations.
*
* <p>This package provides a fluent, chainable interface for interacting with Cloudflare DNS
* records, making code more readable and concise.
*
* <p>Example usage:
* <pre><code>
* // Create a DNS record
* client.zone("example.com")
* .record("api")
* .create(RecordType.A, "192.168.1.1", 60);
*
* // Get DNS records
* List&lt;RecordEntity&gt; records = client.zone("example.com")
* .record("www", RecordType.A)
* .get();
*
* // Update a DNS record
* client.zone("example.com")
* .record("api", RecordType.A)
* .update("192.168.1.2");
*
* // Delete DNS records
* client.zone("example.com")
* .record("old-service")
* .delete(RecordType.A, RecordType.AAAA);
* </code></pre>
*/
package codes.thischwa.cf.fluent;
@@ -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.
*
* <p>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.
*
* <ul>
* <li><b>patches</b>: A list of {@link RecordEntity} objects representing partial updates to existing records.
* <li><b>posts</b>: A list of {@link RecordEntity} objects to be created as new DNS records.
* <li><b>puts</b>: A list of {@link RecordEntity} objects representing updates or replacements for existing records.
* <li><b>deletes</b>: A list of {@link RecordEntity} objects with name, type, and content to be removed.
* </ul>
*
* <p>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<RecordEntity> patches;
List<RecordEntity> posts;
List<RecordEntity> puts;
List<RecordEntity> deletes;
@Override
public String getId() {
return "";
}
}
@@ -0,0 +1,18 @@
package codes.thischwa.cf.model;
/**
* Represents a response that contains a single {@link BatchEntry} as the result.
*
* <p>This class is used for API responses where the primary result is a batch entry,
* which includes collections of operations such as patches, posts, puts, and deletes
* performed on DNS record entities.
*
* <p>Extends {@code AbstractSingleResponse} with {@code BatchEntry} as the generic type,
* ensuring that the response result is a batch of operations.
*/
public class BatchResponse extends AbstractSingleResponse<BatchEntry> {
BatchResponse() {
super();
}
}
@@ -21,6 +21,12 @@ import lombok.Data;
*/
@Data
public class PagingRequest {
/**
* 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.
*/
private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 5_000_000;
private int page;
private int perPage;
@@ -42,12 +48,12 @@ public class PagingRequest {
/**
* Creates a default {@code PagingRequest} instance with a page number set to 1 and a high number
* of items per page (5,000,000) to accommodate large dataset requests.
* of items per page to accommodate large dataset requests and effectively retrieve all records.
*
* @return a default {@code PagingRequest} instance with predefined pagination parameters
*/
public static PagingRequest defaultPaging() {
return new PagingRequest(1, 5000000);
return new PagingRequest(1, DEFAULT_ALL_RECORDS_PAGE_SIZE);
}
/**
@@ -16,7 +16,7 @@ import org.jetbrains.annotations.Nullable;
* <li>Content of the DNS record, such as an IP address.
* <li>Flags indicating whether the record is proxiable or proxied.
* <li>TTL (Time-To-Live) for the DNS record.
* <li>A locked status to indicate immutability of the record.
* <li>A locked status to indicate the immutability of the record.
* <li>Zone-specific metadata including zone ID and name.
* <li>Timestamps for creation and modification.
* </ul>
@@ -34,10 +34,14 @@ public class RecordEntity extends AbstractEntity {
private Boolean proxied;
private Integer ttl;
private Boolean locked;
@Nullable private String zoneId;
@Nullable private String zoneName;
@Nullable private LocalDateTime modifiedOn;
@Nullable private LocalDateTime createdOn;
@Nullable
private String zoneId;
@Nullable
private String zoneName;
@Nullable
private LocalDateTime modifiedOn;
@Nullable
private LocalDateTime createdOn;
/**
* Initializes a new instance of the RecordEntity class and invokes the parent constructor from
@@ -55,15 +59,80 @@ 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;
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
*
* @param id the unique identifier for the DNS record
* @param name the name of the DNS record
* @param type the type of the DNS record, represented as a string (e.g., "A", "CNAME")
* @param ttl the time-to-live (TTL) value 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 attributes
* @throws IllegalArgumentException if the type string is not a valid RecordType
*/
public static RecordEntity build(String id, String name, String type, Integer ttl, String content) {
RecordType recordType;
try {
recordType = RecordType.valueOf(type);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: "
+ java.util.Arrays.toString(RecordType.values()), e);
}
RecordEntity rec = build(name, recordType, ttl, content);
rec.setId(id);
return rec;
}
/**
* Retrieves the short name (subdomain) of the DNS record.
* If the name contains a dot ('.'), only the substring before the first dot is returned.
* This is useful for getting the subdomain part of a fully qualified domain name.
*
* @return the short name of the DNS record (substring before the first dot),
* or the full name if no dot is present
*/
public String getSld() {
if (name == null) {
return null;
}
if (zoneName != null && name.endsWith(zoneName)) {
int zoneNameLength = zoneName.length();
int dotSeparatorLength = 1;
return name.substring(0, name.length() - zoneNameLength - dotSeparatorLength);
}
int firstDotPosition = name.indexOf('.');
if (firstDotPosition > 0) {
return name.substring(0, firstDotPosition);
}
return name;
}
}
@@ -6,6 +6,7 @@ import lombok.Data;
/**
* Represents the result of a response with metadata about its success and associated messages or
* errors.
*
* <p>This class provides a structure to capture the outcome of an operation, including:
* <ul>
* <li>Whether the operation was successful.
@@ -17,6 +18,45 @@ import lombok.Data;
@Data
public class ResponseResultInfo {
private boolean success;
private List<String> errors;
private List<Error> errors;
private List<String> messages;
/**
* Represents an error with a specific code and message.
*
* <p>This class is used to encapsulate error information, including a numerical error code
* and a corresponding descriptive message. It is often used as part of a collection of errors
* to provide detailed diagnostics for failed operations or processes.
*/
@Data
public static class Error {
private int code;
private String message;
/**
* Constructs a new instance of the {@code Error} class with default values for its properties.
*
* <p>This no-argument constructor initializes an {@code Error} object without setting
* specific values for the error code or message. It is primarily used when an error needs to
* be created and set up later, or when default values are acceptable.
*/
public Error() {
}
/**
* Constructs an instance of the {@code Error} class with a specified error code and message.
*
* @param code the numerical code representing the error
* @param message the descriptive message providing details about the error
*/
public Error(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return String.format("%d: %s", code, message);
}
}
}
@@ -1,6 +1,8 @@
package codes.thischwa.cf.model;
/** Represents a response model that contains multiple {@link ZoneEntity} instances. */
/**
* Represents a response model that contains multiple {@link ZoneEntity} instances.
*/
public class ZoneMultipleResponse extends AbstractMultipleResponse<ZoneEntity> {
/**
@@ -1,2 +1,5 @@
/** The model of CloudflareDNS-java. */
/**
* The model of CloudflareDNS-java.
*/
package codes.thischwa.cf.model;
@@ -1,2 +1,5 @@
/** The base package of CloudflareDNS-java. */
/**
* The base package of CloudflareDNS-java.
*/
package codes.thischwa.cf;
@@ -1,13 +1,13 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -32,7 +32,7 @@ public class CfClientPenTest {
}
private CfDnsClient newClient() {
return new CfDnsClient(API_EMAIL, API_KEY);
return new CfDnsClient(true, API_EMAIL, API_KEY);
}
@Test
@@ -40,14 +40,14 @@ public class CfClientPenTest {
void testInvalidCredentialsShouldFail() {
// Use syntactically valid but wrong credentials
CfDnsClient badClient = new CfDnsClient("invalid@example.com", UUID.randomUUID().toString());
assertThrows(CloudflareApiException.class, badClient::zoneListAll);
assertThrows(CloudflareApiException.class, badClient::zoneList);
}
@Test
@DisplayName("Malicious SLD inputs must not crash and should throw proper exception type")
void testMaliciousSldPatternsDoNotSucceed() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneInfo(ZONE_STR);
ZoneEntity zone = client.zoneGet(ZONE_STR);
List<String> syntacticallyInvalidSlds =
List.of("; rm -rf /", "| cat /etc/passwd", "`shutdown -h now`",
@@ -58,11 +58,11 @@ public class CfClientPenTest {
"doesnotexist", "abcdef12345", "unwahrscheinlich-" + System.currentTimeMillis());
for (String sld : syntacticallyInvalidSlds) {
assertThrows(IllegalArgumentException.class, () -> client.sldListAll(zone, sld),
assertThrows(IllegalArgumentException.class, () -> client.recordList(zone, sld),
"Should throw IllegalArgumentException for invalid SLD '" + sld + "'");
}
for (String sld : syntacticallyValidOrNotAllowedFromCloudflare) {
assertThrows(CloudflareNotFoundException.class, () -> client.sldListAll(zone, sld),
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, sld),
"Should throw CloudflareNotFoundException for valid but non-existing SLD '" + sld + "'");
}
}
@@ -71,7 +71,7 @@ public class CfClientPenTest {
@DisplayName("Invalid record content and TTL boundaries must be rejected by API")
void testInvalidRecordCreateInputsRejected() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneInfo(ZONE_STR);
ZoneEntity zone = client.zoneGet(ZONE_STR);
String sld = "pentest-" + System.currentTimeMillis();
String fqdn = sld + "." + ZONE_STR;
@@ -106,7 +106,7 @@ public class CfClientPenTest {
@DisplayName("recordDeleteTypeIfExists must be safe on non-existing SLD and types")
void testDeleteTypeIfExistsOnNonExistingIsSafe() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneInfo(ZONE_STR);
ZoneEntity zone = client.zoneGet(ZONE_STR);
String randomSld = "nonexist-" + System.currentTimeMillis();
// Should not throw even if nothing exists
assertDoesNotThrow(
+339 -33
View File
@@ -1,17 +1,23 @@
package codes.thischwa.cf;
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.assertFalse;
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.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -25,7 +31,7 @@ public class CfClientTest {
private static final String API_EMAIL = System.getenv("API_EMAIL");
private static final String API_KEY = System.getenv("API_KEY");
private final CfDnsClient client = new CfDnsClient(API_EMAIL, API_KEY);
private final CfDnsClient client = new CfDnsClient(true, API_EMAIL, API_KEY);
@BeforeAll
static void checkEnv() {
@@ -34,26 +40,72 @@ public class CfClientTest {
}
@Test
void testZoneListAnlFailedSldList() throws Exception {
List<ZoneEntity> zList = client.zoneListAll();
assertEquals(1, zList.size());
assertThrows(CloudflareNotFoundException.class,
() -> client.sldListAll(zList.get(0), "not-existing"));
void testUnknownSld() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, "unknown", RecordType.A));
}
@Test
void testEmptyResultThrowsException() throws Exception {
List<ZoneEntity> zList = client.zoneListAll();
CfDnsClient client = new CfDnsClient(true, API_EMAIL, API_KEY);
void testAddHost() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
try {
// clean-up
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
RecordEntity record = RecordEntity.build(SLD_STR, RecordType.A, TTL, "127.0.0.1");
RecordEntity createdRecord = client.recordCreate(zone, record);
assertNotNull(createdRecord.getId());
assertEquals(SLD_STR, createdRecord.getSld());
assertEquals(RecordType.A.getType(), createdRecord.getType());
assertEquals(TTL, createdRecord.getTtl());
assertEquals("127.0.0.1", createdRecord.getContent());
assertNotNull(createdRecord.getCreatedOn());
List<RecordEntity> records = client.recordList(zone, SLD_STR, RecordType.A);
assertEquals(1, records.size());
RecordEntity fetchedRecord = records.get(0);
assertEquals(createdRecord.getId(), fetchedRecord.getId());
assertEquals(createdRecord.getContent(), fetchedRecord.getContent());
assertEquals(createdRecord.getType(), fetchedRecord.getType());
client.recordDelete(zone, createdRecord);
// test A and AAAA records for the same SLD
RecordEntity recordA = RecordEntity.build(SLD_STR, RecordType.A, TTL, "127.0.0.2");
RecordEntity recordAAAA = RecordEntity.build(SLD_STR, RecordType.AAAA, TTL, "2001:db8::1");
RecordEntity createdRecordA = client.recordCreate(zone, recordA);
RecordEntity createdRecordAAAA = client.recordCreate(zone, recordAAAA);
assertNotNull(createdRecordA.getId());
assertNotNull(createdRecordAAAA.getId());
assertEquals(SLD_STR, createdRecordA.getSld());
assertEquals(SLD_STR, createdRecordAAAA.getSld());
assertEquals(RecordType.A.getType(), createdRecordA.getType());
assertEquals(RecordType.AAAA.getType(), createdRecordAAAA.getType());
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
assertThrows(CloudflareNotFoundException.class,
() -> client.sldListAll(zList.get(0), "not-existing"));
() -> client.recordList(zone, SLD_STR, RecordType.A, RecordType.AAAA));
} finally {
// cleanup in case of failures during test
try {
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testZoneListAnlFailedSldList() throws Exception {
List<ZoneEntity> zList = client.zoneList();
assertEquals(1, zList.size());
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zList.get(0), "not-existing"));
}
@Test
void testDns() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity z = client.zoneInfo(ZONE_STR);
ZoneEntity z = client.zoneGet(ZONE_STR);
assertEquals("0a83dd6e7f8c46039f2517bbded8115e", z.getId());
assertEquals("mein-d-ns.de", z.getName());
assertEquals("active", z.getStatus());
@@ -81,63 +133,93 @@ public class CfClientTest {
createdRe1 =
client.recordCreate(z, RecordEntity.build(domain, RecordType.A, TTL, "130.0.0.3"));
assertNotNull(createdRe1.getId());
assertEquals(domain, createdRe1.getName());
assertEquals(randomSld + "." + ZONE_STR, createdRe1.getName());
assertEquals(randomSld, createdRe1.getSld());
assertEquals(RecordType.A.getType(), createdRe1.getType());
assertEquals(z.getId(), createdRe1.getZoneId());
assertEquals(TTL, createdRe1.getTtl());
assertEquals("130.0.0.3", createdRe1.getContent());
assertNotNull(createdRe1.getCreatedOn());
assertNotNull(createdRe1.getModifiedOn());
// verify sldInfo for A
r = client.sldInfo(z, randomSld, RecordType.A);
// verify recordList for A
List<RecordEntity> aRecords = client.recordList(z, randomSld, RecordType.A);
assertEquals(1, aRecords.size());
r = aRecords.get(0);
assertEquals("130.0.0.3", r.getContent());
// create AAAA record using recordCreateSld
createdRe2 =
client.recordCreateSld(z, randomSld, TTL, RecordType.AAAA, "2a0a:4cc0:c0:2e4::1");
r = client.sldInfo(z, randomSld, RecordType.AAAA);
List<RecordEntity> aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA);
assertEquals(1, aaaaRecords.size());
r = aaaaRecords.get(0);
assertEquals(z.getId(), r.getZoneId());
assertEquals("2a0a:4cc0:c0:2e4::1", r.getContent());
assertEquals(RecordType.AAAA.getType(), r.getType());
// test sldListAll
List<RecordEntity> rList = client.sldListAll(z, randomSld);
// test recordList
List<RecordEntity> rList = client.recordList(z, randomSld);
assertEquals(2, rList.size());
for (RecordEntity re : rList) {
assertEquals(z.getId(), re.getZoneId());
if (Objects.equals(re.getType(), RecordType.A.getType())) {
assertEquals("130.0.0.3", re.getContent());
} else if (Objects.equals(re.getType(), RecordType.AAAA.getType())) {
assertEquals("2a0a:4cc0:c0:2e4::1", re.getContent());
} else {
throw new IllegalStateException("Unexpected record type: " + re.getType());
fail(String.format("Unexpected record type: %s", re.getType()));
}
}
// test recordList without SLD
List<RecordEntity> fullList = client.recordList(z);
assertTrue(fullList.size() >= 2);
assertTrue(fullList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(fullList.stream().anyMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(
fullList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType()) || re.getType().equals(RecordType.AAAA.getType())));
// test recordList with types without SLD
List<RecordEntity> aList = client.recordList(z, RecordType.A);
assertFalse(aList.isEmpty());
assertTrue(aList.size() >= 1);
assertTrue(aList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(aList.stream().noneMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(aList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType())));
// test fluent api list
List<RecordEntity> fluentList = client.zone(ZONE_STR).list(RecordType.A);
assertFalse(fluentList.isEmpty());
assertTrue(fluentList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
// update AAAA record
createdRe2.setContent("2a0a:4cc0:c0:2e4::2");
client.recordUpdate(z, createdRe2);
r = client.sldInfo(z, randomSld, RecordType.AAAA);
aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA);
assertEquals(1, aaaaRecords.size());
r = aaaaRecords.get(0);
assertEquals("2a0a:4cc0:c0:2e4::2", r.getContent());
// verify A record still intact
r = client.sldInfo(z, randomSld, RecordType.A);
aRecords = client.recordList(z, randomSld, RecordType.A);
assertEquals(1, aRecords.size());
r = aRecords.get(0);
assertEquals("130.0.0.3", r.getContent());
// delete AAAA record and verify it's gone
assertTrue(client.recordDelete(z, createdRe2));
assertThrows(CloudflareNotFoundException.class,
() -> client.sldInfo(z, randomSld, RecordType.AAAA));
() -> client.recordList(z, randomSld, RecordType.AAAA));
// delete A record using helper and verify it's gone
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A);
assertThrows(CloudflareNotFoundException.class,
() -> client.sldInfo(z, randomSld, RecordType.A));
() -> client.recordList(z, randomSld, RecordType.A));
} finally {
// cleanup in case of failures during test
try {
client.recordDeleteTypeIfExists(z, randomSld, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
try {
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A);
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
}
}
@@ -149,4 +231,228 @@ public class CfClientTest {
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", ""));
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("", "key"));
}
@Test
void testRecordEntityInvalidType() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> RecordEntity.build("id123", "example.com", "INVALID_TYPE", 60, "192.168.1.1"));
assertTrue(exception.getMessage().contains("Invalid record type: INVALID_TYPE"));
assertTrue(exception.getMessage().contains("Must be one of:"));
}
private static final String IP_PREFIX = "130.0.0.";
private static final String UPDATED_IP_PREFIX = "130.1.0.";
@Test
void testBatch() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity zone = client.zoneGet(ZONE_STR);
List<String> sldNames = createSldNames();
List<RecordEntity> initialRecords = createInitialRecords(sldNames);
cleanupRecords(zone, sldNames);
try {
testBatchPost(zone, initialRecords, sldNames);
testBatchPatch(zone, sldNames);
testBatchDelete(zone, sldNames);
testBatchPut(zone, sldNames);
} finally {
cleanupRecords(zone, sldNames);
}
}
private List<String> createSldNames() {
return List.of(SLD_STR + "-1", SLD_STR + "-2", SLD_STR + "-3");
}
private List<RecordEntity> createInitialRecords(List<String> sldNames) {
List<RecordEntity> records = new ArrayList<>();
for (int i = 0; i < sldNames.size(); i++) {
records.add(RecordEntity.build(sldNames.get(i), RecordType.A, TTL, IP_PREFIX + (i + 1)));
}
return records;
}
private void cleanupRecords(ZoneEntity zone, List<String> sldNames) {
sldNames.forEach(sld -> {
try {
client.recordDeleteTypeIfExists(zone, sld, RecordType.A);
} catch (CloudflareApiException e) {
throw new RuntimeException(e);
}
});
}
private void testBatchPost(ZoneEntity zone, List<RecordEntity> records, List<String> sldNames) throws Exception {
// Use only first 2 records for POST
List<RecordEntity> postRecords = records.subList(0, 2);
BatchEntry batchEntry = client.recordBatch(zone, postRecords, null, null, null);
assertEquals(2, batchEntry.getPosts().size());
RecordEntity batchedRecord = batchEntry.getPosts().get(0);
assertValidBatchedRecord(batchedRecord, postRecords.get(0));
// Verify only the first 2 records
for (int i = 0; i < 2; i++) {
List<RecordEntity> records1 = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, records1.size());
assertEquals(IP_PREFIX + (i + 1), records1.get(0).getContent());
}
}
private void testBatchPatch(ZoneEntity zone, List<String> sldNames) throws Exception {
// Use first 2 records for PATCH
List<RecordEntity> patchRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
RecordEntity record = records.get(0);
record.setContent(UPDATED_IP_PREFIX + (i + 1));
patchRecords.add(record);
}
client.recordBatch(zone, null, null, patchRecords, null);
// Verify both records were updated
for (int i = 0; i < 2; i++) {
List<RecordEntity> updatedRecords = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, updatedRecords.size());
assertEquals(UPDATED_IP_PREFIX + (i + 1), updatedRecords.get(0).getContent());
}
}
private void testBatchDelete(ZoneEntity zone, List<String> sldNames) throws Exception {
// Delete first 2 records
List<RecordEntity> deleteRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
deleteRecords.add(records.get(0));
}
client.recordBatch(zone, null, null, null, deleteRecords);
// Verify both records are deleted
for (int i = 0; i < 2; i++) {
String sldName = sldNames.get(i);
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zone, sldName, RecordType.A));
}
}
private void testBatchPut(ZoneEntity zone, List<String> sldNames) throws Exception {
// Create 2 new records first for PUT test
List<RecordEntity> newRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
RecordEntity record = RecordEntity.build(sldNames.get(i), RecordType.A, TTL, IP_PREFIX + (i + 1));
newRecords.add(record);
}
client.recordBatch(zone, newRecords, null, null, null);
// Now use PUT to replace them
List<RecordEntity> putRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
RecordEntity record = records.get(0);
record.setContent(UPDATED_IP_PREFIX + (i + 1));
putRecords.add(record);
}
client.recordBatch(zone, null, putRecords, null, null);
// Verify both records were updated
for (int i = 0; i < 2; i++) {
List<RecordEntity> updatedRecords = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, updatedRecords.size());
assertEquals(UPDATED_IP_PREFIX + (i + 1), updatedRecords.get(0).getContent());
}
}
private void assertValidBatchedRecord(RecordEntity batchedRecord, RecordEntity originalRecord) {
assertNotNull(batchedRecord.getId());
assertEquals(originalRecord.getSld(), batchedRecord.getSld());
assertEquals(originalRecord.getType(), batchedRecord.getType());
assertNotNull(batchedRecord.getCreatedOn());
}
@Test
void testFluentApi() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
String fluentSld = "fluent-" + System.currentTimeMillis();
try {
// Test fluent create
RecordEntity created = client.zone(ZONE_STR)
.record(fluentSld)
.create(RecordType.A, "192.168.100.1", TTL);
assertNotNull(created.getId());
assertEquals(fluentSld, created.getSld());
assertEquals("192.168.100.1", created.getContent());
// Test fluent get
List<RecordEntity> records = client.zone(ZONE_STR)
.record(fluentSld, RecordType.A)
.get();
assertEquals(1, records.size());
assertEquals("192.168.100.1", records.get(0).getContent());
// Test fluent update
RecordEntity updated = client.zone(ZONE_STR)
.record(fluentSld, RecordType.A)
.update("192.168.100.2");
assertEquals("192.168.100.2", updated.getContent());
// Test fluent delete
client.zone(ZONE_STR)
.record(fluentSld)
.delete(RecordType.A);
assertThrows(CloudflareNotFoundException.class,
() -> client.zone(ZONE_STR).record(fluentSld, RecordType.A).get());
} finally {
try {
client.recordDeleteTypeIfExists(zone, fluentSld, RecordType.A);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testGroupRecordsByFqdn_withValidRecords() {
// Arrange
RecordEntity rec1 = RecordEntity.build("1", "example.com.", "A", 300, "192.168.1.1");
RecordEntity rec2 = RecordEntity.build("2", "example.com.", "AAAA", 300, "::1");
RecordEntity rec3 = RecordEntity.build("3", "sub.example.com.", "CNAME", 300, "example.com.");
List<RecordEntity> records = Arrays.asList(rec1, rec2, rec3);
// Act
Map<String, List<RecordEntity>> groupedRecords = CfDnsClient.groupRecordsByFqdn(records);
// Assert
assertNotNull(groupedRecords, "Resulting map should not be null.");
assertEquals(2, groupedRecords.size(), "The grouping should result in 2 FQDN keys.");
assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records.");
assertEquals(1, groupedRecords.get("sub.example.com.").size(), "The key 'sub.example.com.' should have 1 record.");
}
@Test
void testGroupRecordsByFqdn_withMultipleRecordsSameFqdn() {
// Arrange
RecordEntity rec1 = RecordEntity.build("1", "example.com.", "A", 300, "192.168.1.1");
RecordEntity rec2 = RecordEntity.build("2", "example.com.", "AAAA", 300, "::1");
List<RecordEntity> records = Arrays.asList(rec1, rec2);
// Act
Map<String, List<RecordEntity>> groupedRecords = CfDnsClient.groupRecordsByFqdn(records);
// Assert
assertNotNull(groupedRecords, "Resulting map should not be null.");
assertEquals(1, groupedRecords.size(), "The grouping should result in 1 FQDN key.");
assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records.");
}
}
@@ -3,7 +3,6 @@ package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import codes.thischwa.cf.model.RecordType;
import org.junit.jupiter.api.Test;
public class CfRequestTest {
@@ -46,14 +45,14 @@ public class CfRequestTest {
@Test
public void testBuildRecordInfo() {
String result = CfRequest.RECORD_INFO_NAME_TYPE.buildPath("zone123", "sld.domain.com", RecordType.A);
assertEquals("/zones/zone123/dns_records?name=sld.domain.com&type=A", result);
String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sld.domain.com");
assertEquals("/zones/zone123/dns_records?name=sld.domain.com", result);
}
@Test
public void testBuildPathInvalidArguments() {
assertThrows(
IllegalArgumentException.class,
() -> CfRequest.RECORD_INFO_NAME_TYPE.buildPath("zone123", "sld.domain.com"));
() -> CfRequest.RECORD_UPDATE.buildPath("zone123"));
}
}
@@ -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);
assertNotNull(resp.getResponseResultInfo());
}
@Test
void testErrorResponse() throws IOException {
List<Class<? extends AbstractResponse>> respClasses =
List.of(RecordSingleResponse.class, RecordMultipleResponse.class, ZoneMultipleResponse.class, BatchResponse.class);
respClasses.forEach(this::assertErrorResponse);
}
private void assertErrorResponse(Class<? extends AbstractResponse> 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());
}
}
}
@@ -1,16 +1,18 @@
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.RecordEntity;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
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;
@@ -29,6 +31,12 @@ class ResponseValidatorTest {
@Mock
private RecordMultipleResponse mockMultipleResponse;
@Mock
private RecordSingleResponse mockSingleResponse;
@Mock
private RecordEntity mockRecordEntity;
private ResponseValidator validatorWithException;
private ResponseValidator validatorWithoutException;
@@ -48,9 +56,15 @@ class ResponseValidatorTest {
@Test
void validateFailedResponse() {
List<ResponseResultInfo.Error> 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));
@@ -87,4 +101,33 @@ class ResponseValidatorTest {
assertDoesNotThrow(() -> validatorWithoutException.validate(mockMultipleResponse, true));
}
@Test
void validateSingleResultWithNullResultAndExceptionEnabled() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockSingleResponse.getResult()).thenReturn(null);
assertThrows(CloudflareNotFoundException.class,
() -> validatorWithException.validate(mockSingleResponse, false));
}
@Test
void validateSingleResultWithNullResultAndExceptionDisabled() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
// mockSingleResponse.getResult() returns null by default (no stubbing needed)
assertDoesNotThrow(() -> validatorWithoutException.validate(mockSingleResponse, false));
}
@Test
void validateSingleResultWithValidResult() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockSingleResponse.getResult()).thenReturn(mockRecordEntity);
assertDoesNotThrow(() -> validatorWithException.validate(mockSingleResponse, false));
assertDoesNotThrow(() -> validatorWithoutException.validate(mockSingleResponse, false));
}
}
+2 -1
View File
@@ -8,7 +8,8 @@
</encoder>
</appender>
<logger name="org.apache.hc.client5.http" level="info" />
<logger name="org.apache.hc.client5.http" level="info"/>
<logger name="codes.thischwa.cf.CfBasicHttpClient" level="trace"/>
<root level="debug">
<appender-ref ref="current"/>