Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d052555ba7 | |||
| dfa2753619 | |||
| 0a47bb6338 | |||
| 25228af9f0 | |||
| d4fba87e13 | |||
| 9919230a3b | |||
| ba9bc9ea93 | |||
| eea6e72145 | |||
| c68b865798 | |||
| 41d35fe745 | |||
| c2d10bb929 | |||
| f3e05e1bc7 | |||
| 90664f4007 | |||
| 2c233437da | |||
| 84c5295345 | |||
| 6d139329f1 | |||
| b5caee443e | |||
| 053979d4e1 | |||
| d57da1e60f | |||
| 3613df6974 | |||
| 0acf0d0834 | |||
| 5ed9da4036 | |||
| 84a37b8a8d | |||
| c292c27444 | |||
| 744abd47ff | |||
| 9f301337b6 | |||
| 0a401b164c | |||
| 7b08820d1a | |||
| 83995ba5fa | |||
| d586567fd6 | |||
| 3505aee124 | |||
| ee3dcda8ea | |||
| c01946fc05 | |||
| 09cb00db19 | |||
| a5cf641d27 | |||
| 2a76c9e469 | |||
| 8bbcebc53c | |||
| ac1fd02b2f | |||
| e42579541a | |||
| f180e7daba | |||
| 0ce37c53aa | |||
| 60005d7d6e | |||
| 4e7b3b9bdb | |||
| 66a5d48927 | |||
| 6e71ba266a | |||
| be409139d7 | |||
| 8d2fc74d04 | |||
| aaae19f783 | |||
| 092412cf92 | |||
| abb9704a87 | |||
| 6a11868a0b | |||
| f016067931 | |||
| c857c0d233 | |||
| dd1052cd75 | |||
| 32618a942b | |||
| 4d7deb3f2c | |||
| 85f462e002 | |||
| d1e2a78f24 | |||
| 233c3988bc | |||
| b199495d55 | |||
| 8b69b2551a | |||
| eb2a96a482 | |||
| b864e019c1 | |||
| fc7952f8a2 | |||
| 378c3e5d9b | |||
| a6b76a00bf | |||
| 9a525ace3b | |||
| baf6c6a8a3 | |||
| 729799faef | |||
| d7d49d10fb | |||
| 813cb3a65b | |||
| 78310e5639 | |||
| 3b83e65e21 | |||
| ce460ee4d8 | |||
| a3b1a542b2 | |||
| 163a096f81 | |||
| a0016d7ca3 | |||
| e1600d1b4e | |||
| 116350b65d | |||
| 9d41ddc2e5 | |||
| 53b10b142e | |||
| 6abd0ce862 | |||
| 579aa4971b | |||
| 725d8e5698 | |||
| 2b359cbd88 | |||
| 7b83b99cf1 | |||
| e90f4406e8 | |||
| 34b3f98d37 | |||
| eb8eb1ca43 | |||
| 85b71b851d | |||
| 2480d12a16 | |||
| 880a6c7380 | |||
| 69bbda1493 | |||
| cd2bc207ef | |||
| d83c364660 | |||
| 2efcb24321 | |||
| 62b86ca22f | |||
| f12f5d6db1 | |||
| c435a80966 | |||
| 1e8fe53891 | |||
| 10ad4bd7f3 | |||
| 84a454f656 | |||
| 4a3ec42fa5 | |||
| 9e09b12dc3 | |||
| d0802fc01d | |||
| 607b634b35 | |||
| b593ca7311 | |||
| 773573bd39 | |||
| b890ae9d17 | |||
| c80cfc8b66 | |||
| b07c818127 | |||
| 249dfd2de5 | |||
| b241d8cf09 | |||
| b88f4f78ba | |||
| 9e8ee5bb4a | |||
| 34010a4e77 | |||
| 0f1248d08b | |||
| acf2a2fc3b | |||
| 483b79b372 | |||
| 5a6a17798b | |||
| edf1752e81 | |||
| 25fd480e69 | |||
| 5098e17172 | |||
| a221de4792 | |||
| 1bfea09aa9 | |||
| 12c75914b1 | |||
| dd586be9f4 | |||
| 77a584afb6 | |||
| 6027e66afe | |||
| 351730dc79 | |||
| c75f9d74ca | |||
| 1abe888a65 | |||
| 80670c2a90 | |||
| ce1f766326 | |||
| a965f21d6b | |||
| a3dc89e2c5 | |||
| 9c36fa1988 | |||
| 335c9a6c5e | |||
| 9dfb5f4c75 | |||
| e12eaff137 | |||
| 596006276e | |||
| 43ff58febc | |||
| f2f7f662a1 | |||
| 9a376415b3 | |||
| 0eaca55e95 | |||
| deb6fcf445 | |||
| e18f8d7017 | |||
| 4df6757ee4 | |||
| 3d026dab1d | |||
| c20bb6bb1c | |||
| a5e96f38f8 | |||
| 4cb9b9929e | |||
| 67df00c36e | |||
| 7f56c6f453 | |||
| a5d7992391 | |||
| b4ce867efc | |||
| eb821a2218 |
@@ -0,0 +1,120 @@
|
||||
name: Publish JUnit Report (Short Names)
|
||||
description: Normalize JUnit XML report names and publish a test report to the repo.
|
||||
inputs:
|
||||
token:
|
||||
description: Gitea token with write access to the repository.
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Normalize JUnit report names
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p junit-short
|
||||
shopt -s globstar nullglob
|
||||
for f in **/target/*-reports/TEST-*.xml; do
|
||||
base="$(basename "$f")"
|
||||
short="${base#TEST-}"
|
||||
cp "$f" "junit-short/${short}"
|
||||
done
|
||||
|
||||
- name: Generate Markdown Test Report
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p reports
|
||||
python3 - <<'EOF'
|
||||
import glob, xml.etree.ElementTree as ET
|
||||
|
||||
# --- JUnit ---
|
||||
files = glob.glob("junit-short/*.xml")
|
||||
total_tests = total_failures = total_errors = total_skipped = 0
|
||||
test_rows = []
|
||||
|
||||
for f in sorted(files):
|
||||
tree = ET.parse(f)
|
||||
r = tree.getroot()
|
||||
suites = r.findall("testsuite") if r.tag == "testsuites" else [r]
|
||||
for ts in suites:
|
||||
name = ts.get("name", f)
|
||||
tests = int(ts.get("tests", 0))
|
||||
failures = int(ts.get("failures", 0))
|
||||
errors = int(ts.get("errors", 0))
|
||||
skipped = int(ts.get("skipped", 0))
|
||||
passed = tests - failures - errors - skipped
|
||||
status = "✅" if (failures + errors) == 0 else "❌"
|
||||
total_tests += tests
|
||||
total_failures += failures
|
||||
total_errors += errors
|
||||
total_skipped += skipped
|
||||
test_rows.append(f"| {status} | {name} | {tests} | {passed} | {failures + errors} | {skipped} |")
|
||||
|
||||
total_passed = total_tests - total_failures - total_errors - total_skipped
|
||||
overall = "✅ All tests passed" if (total_failures + total_errors) == 0 else "❌ Some tests failed"
|
||||
|
||||
# --- JaCoCo ---
|
||||
def counter(el, type_):
|
||||
c = next((x for x in el.findall("counter") if x.get("type") == type_), None)
|
||||
if c is None:
|
||||
return 0, 0
|
||||
covered = int(c.get("covered", 0))
|
||||
missed = int(c.get("missed", 0))
|
||||
return covered, covered + missed
|
||||
|
||||
cov_rows = []
|
||||
jacoco_files = glob.glob("**/target/site/jacoco/jacoco.xml", recursive=True)
|
||||
|
||||
for jf in sorted(jacoco_files):
|
||||
tree = ET.parse(jf)
|
||||
root = tree.getroot()
|
||||
for pkg in root.findall("package"):
|
||||
name = pkg.get("name", "").replace("/", ".")
|
||||
line_cov, line_total = counter(pkg, "LINE")
|
||||
branch_cov, branch_total = counter(pkg, "BRANCH")
|
||||
line_pct = f"{100 * line_cov / line_total:.0f}%" if line_total else "n/a"
|
||||
branch_pct = f"{100 * branch_cov / branch_total:.0f}%" if branch_total else "n/a"
|
||||
cov_rows.append(f"| {name} | {line_pct} ({line_cov}/{line_total}) | {branch_pct} ({branch_cov}/{branch_total}) |")
|
||||
|
||||
# --- Markdown zusammenbauen ---
|
||||
md = (
|
||||
"# Test Report\n\n"
|
||||
f"**{overall}**\n\n"
|
||||
"## Test Results\n\n"
|
||||
"| | Tests | Passed | Failed | Skipped |\n"
|
||||
"|---|---|---|---|---|\n"
|
||||
f"| **Total** | {total_tests} | {total_passed} | {total_failures + total_errors} | {total_skipped} |\n\n"
|
||||
"### Details\n\n"
|
||||
"| Status | Suite | Tests | Passed | Failed | Skipped |\n"
|
||||
"|---|---|---|---|---|---|\n"
|
||||
) + "\n".join(test_rows) + "\n\n"
|
||||
|
||||
if cov_rows:
|
||||
md += (
|
||||
"## Coverage\n\n"
|
||||
"| Package | Line Coverage | Branch Coverage |\n"
|
||||
"|---|---|---|\n"
|
||||
) + "\n".join(cov_rows) + "\n"
|
||||
else:
|
||||
md += "_No JaCoCo report found._\n"
|
||||
|
||||
with open("reports/test.md", "w") as out:
|
||||
out.write(md)
|
||||
print(md)
|
||||
EOF
|
||||
|
||||
- name: Commit Test-Report to Repo
|
||||
shell: bash
|
||||
env:
|
||||
GIT_USER: gitea-actions-bot
|
||||
GIT_EMAIL: bot@mein-gateway.de
|
||||
GITEA_TOKEN: ${{ inputs.token }}
|
||||
run: |
|
||||
git config user.name "$GIT_USER"
|
||||
git config user.email "$GIT_EMAIL"
|
||||
git remote set-url origin https://$GIT_USER:$GITEA_TOKEN@git.mein-gateway.de/${{ github.repository }}.git
|
||||
git fetch origin
|
||||
git checkout ${{ github.ref_name }}
|
||||
git add reports/
|
||||
git diff --cached --quiet && echo "No changes" && exit 0
|
||||
git commit -m "ci: update test report [skip ci]"
|
||||
git push origin ${{ github.ref_name }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: "Setup Maven with GitHub Packages"
|
||||
|
||||
description: "Sets up JDK and caches Maven dependencies."
|
||||
|
||||
inputs:
|
||||
java-version:
|
||||
description: "Java version to install"
|
||||
default: "17"
|
||||
required: true
|
||||
java-distribution:
|
||||
description: "Java distribution to use"
|
||||
default: "corretto"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up JDK
|
||||
with:
|
||||
distribution: ${{ inputs.java-distribution }}
|
||||
java-version: ${{ inputs.java-version }}
|
||||
uses: actions/setup-java@v5
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Build and Analyse
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- feature*
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened ]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
|
||||
build-and-analyse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Setup Java and Maven
|
||||
uses: ./.gitea/actions/setup-java-maven
|
||||
|
||||
- name: Build and test
|
||||
run: mvn -B verify
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: ./.gitea/actions/publish-report
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,75 +0,0 @@
|
||||
default:
|
||||
image: maven:3-amazoncorretto-17-alpine
|
||||
|
||||
stages:
|
||||
- build
|
||||
- on_commit
|
||||
- sonarcloud_scan
|
||||
- deploy
|
||||
- release
|
||||
|
||||
variables:
|
||||
GITLAB_CLONE_DIR: "/builds/th-schwarz/CloudflareDNS-java"
|
||||
GITLAB_USERNAME: $GITLAB_USERNAME
|
||||
GITLAB_USEREMAIL: GITLAB_USEREMAIL
|
||||
SONAR_HOST_URL: $SONAR_HOST_URL
|
||||
SONAR_PROJECT_KEY: "thischwa_CloudflareDNS-java"
|
||||
SONAR_ORGANIZATION: "thischwa"
|
||||
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
|
||||
API_EMAIL: $API_EMAIL
|
||||
API_KEY: $API_KEY
|
||||
API_TOKEN: $API_TOKEN
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- echo "Running package..."
|
||||
- cd ${GITLAB_CLONE_DIR}
|
||||
- mvn clean package
|
||||
- echo "Preparing GitLab Pages from docs/ (javadoc)"
|
||||
- mkdir public
|
||||
- cp -rv docs/* public/
|
||||
- mkdir public/apidocs
|
||||
- cp -rv target/apidocs public/
|
||||
artifacts:
|
||||
paths:
|
||||
- target/surefire-reports/*.xml
|
||||
- target/
|
||||
- public/
|
||||
except:
|
||||
- tags
|
||||
|
||||
on_commits:
|
||||
stage: on_commit
|
||||
dependencies:
|
||||
- build
|
||||
script:
|
||||
- echo "on_commit DONE"
|
||||
only:
|
||||
- /^feature.*$/
|
||||
- merge_request
|
||||
- develop
|
||||
|
||||
pages:
|
||||
# triggers the page deployment of gitlab
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Publishing to GitLab Pages ..."
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- develop
|
||||
|
||||
sonarcloud_scan:
|
||||
stage: sonarcloud_scan
|
||||
dependencies:
|
||||
- build
|
||||
script:
|
||||
- mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
|
||||
only:
|
||||
- merge_requests
|
||||
- develop
|
||||
- tags
|
||||
@@ -1,21 +1,12 @@
|
||||
# CloudflareDNS-java
|
||||
|
||||

|
||||

|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
|
||||
[](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
|
||||
[](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
|
||||
[](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
|
||||
[](https://sonarcloud.io/summary/new_code?id=thischwa_CloudflareDNS-java)
|
||||
|
||||
## Preface
|
||||
|
||||
This project provides a java client for minimalistic access to the Cloudflare API version 4, which is mainly used for
|
||||
managing DNS settings such as creating, updating and deleting DNS records.
|
||||
|
||||
If you encounter any bugs or find missing features, feel free to report them on
|
||||
the [GitLab Issues page](https://gitlab.com/th-schwarz/CloudflareDNS-java/-/issues).
|
||||
the [Gitea Issues page](https://git.mein-gateway.de/thischwa/CloudflareDNS-java/issues).
|
||||
|
||||
---
|
||||
|
||||
@@ -27,34 +18,11 @@ This guide comes without any warranty. Use at your own risk. The author is not r
|
||||
|
||||
## Get It
|
||||
|
||||
The project has its own maven repository. It can be added to the `pom.xml`:
|
||||
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>gitlab-cloudflare</id>
|
||||
<url>https://gitlab.com/api/v4/projects/68509751/packages/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
|
||||
The dependency is:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>codes.thischwa</groupId>
|
||||
<artifactId>cloudflaredns</artifactId>
|
||||
<version>[version]</version>
|
||||
</dependency>
|
||||
```
|
||||
The project has its own maven repository. Follow the instructions on the latest [package](https://git.mein-gateway.de/thischwa/-/packages) to add the repository to your project.
|
||||
|
||||
## Changelog
|
||||
|
||||
- 0.1.0:
|
||||
- refactored / extended tests
|
||||
- 0.1.0-beta.3:
|
||||
- fixed json deserialization
|
||||
- added logging of api errors
|
||||
- 0.1.0-beta.1: 1st runnable version
|
||||
See [changelog](changelog.md)
|
||||
|
||||
## Methods Overview
|
||||
|
||||
@@ -63,31 +31,44 @@ 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).
|
||||
|
||||
### Instantiation of `CfDnsClient`
|
||||
|
||||
#### With API Token (recommended):
|
||||
```java
|
||||
CfDnsClient cfDnsClient = new CfDnsClient(
|
||||
"email@example.com", "yourApiKey"
|
||||
);
|
||||
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("your-api-token")
|
||||
.build();
|
||||
```
|
||||
|
||||
### `zoneListAll`
|
||||
#### With Email/Key (legacy):
|
||||
```java
|
||||
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
.withEmailKeyAuth("email@example.com", "yourApiKey")
|
||||
.build();
|
||||
```
|
||||
|
||||
### `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 +77,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 +96,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");
|
||||
records.forEach(record ->
|
||||
System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
|
||||
List<RecordEntity> records = cfDnsClient.recordList(zone, "sld");
|
||||
records.forEach(record ->
|
||||
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,33 +188,226 @@ 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 getRecord
|
||||
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 getRecord
|
||||
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 CfDnsClientBuilder()
|
||||
.withApiTokenAuth("your-api-token")
|
||||
.build();
|
||||
|
||||
// Create a new getRecord
|
||||
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 getRecord
|
||||
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 CfDnsClientBuilder()
|
||||
.withApiTokenAuth("your-api-token")
|
||||
.withEmptyResultThrowsException(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
## 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");
|
||||
} else {
|
||||
log.error("Error while getting sld info of www", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
|
||||
`CfDnsClient` offers a simple interface for managing DNS entries via Cloudflare's public API, allowing seamless CRUD
|
||||
operations and automation-friendly workflows.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
- 0.5.0-SNAPSHOT:
|
||||
- moved the project to git.mein-gateway.de
|
||||
- replaced sonarqube with own actions
|
||||
- 0.4.0:
|
||||
- fixed some paging issues
|
||||
- **Breaking Change**: renamed `client.zone().record()` to `client.zone().getRecord()`
|
||||
- Code quality improvements: Increasing test coverage
|
||||
- 0.3.0:
|
||||
- **Breaking Change**:
|
||||
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`)
|
||||
- Authentication with API token.
|
||||
- 0.2.0:
|
||||
- **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()`
|
||||
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`) and added chainable method interface for more readable DNS operations (
|
||||
`client.zone().record()...`)
|
||||
- Code quality improvements: removed 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:
|
||||
- fixed json deserialization
|
||||
- added logging of api errors
|
||||
- 0.1.0-beta.1: 1st runnable version
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cloudflare DNS Client - java</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Cloudflare DNS Client - java</h1>
|
||||
<p>A Java-based client for interacting with the Cloudflare DNS API.</p>
|
||||
<p>
|
||||
<a href="apidocs/index.html" target="_blank">View API Documentation</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,233 +1,278 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>codes.thischwa</groupId>
|
||||
<artifactId>cloudflaredns</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<name>CloudflareDNS-java</name>
|
||||
<inceptionYear>2025</inceptionYear>
|
||||
<packaging>jar</packaging>
|
||||
<groupId>codes.thischwa</groupId>
|
||||
<artifactId>cloudflaredns</artifactId>
|
||||
<version>0.5.0-SNAPSHOT</version>
|
||||
<name>CloudflareDNS-java</name>
|
||||
<inceptionYear>2025</inceptionYear>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<issueManagement>
|
||||
<url>https://gitlab.com/th-schwarz/CloudflareDNS-java/-/issues</url>
|
||||
<system>GitLab Issues</system>
|
||||
</issueManagement>
|
||||
<issueManagement>
|
||||
<url>https://git.mein-gateway.de/thischwa/CloudflareDNS-java/issues</url>
|
||||
<system>Gitea Issues</system>
|
||||
</issueManagement>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<file.encoding>UTF-8</file.encoding>
|
||||
<project.build.sourceEncoding>${file.encoding}</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<file.encoding>UTF-8</file.encoding>
|
||||
<project.build.sourceEncoding>${file.encoding}</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
|
||||
|
||||
<!-- checkstyle -->
|
||||
<checkstyle.version>10.21.3</checkstyle.version>
|
||||
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
|
||||
<checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml
|
||||
</checkstyle.config.location>
|
||||
<checkstyle.includeTestResources>false</checkstyle.includeTestResources>
|
||||
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
|
||||
<checkstyle.failOnViolation>false</checkstyle.failOnViolation>
|
||||
<checkstyle.consoleOutput>true</checkstyle.consoleOutput>
|
||||
<linkX-Ref>false</linkX-Ref>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<jackson.version>2.18.2</jackson.version>
|
||||
<httpclient5.version>5.4.3</httpclient5.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<slf4j.version>2.0.17</slf4j.version>
|
||||
<logback-classic.version>1.5.18</logback-classic.version>
|
||||
<junit5.version>5.12.2</junit5.version>
|
||||
<mockito-junit5.version>5.17.0</mockito-junit5.version>
|
||||
|
||||
<!-- sonarqube -->
|
||||
<sonar.organization>thischwa</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.projectName>CloudflareDNS-java</sonar.projectName>
|
||||
<sonar.branch.name>develop</sonar.branch.name>
|
||||
<sonar.test.exclusions>src/test/java/**/*</sonar.test.exclusions>
|
||||
</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>
|
||||
</scm>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>gitlab-cloudflaredns</id>
|
||||
<name>GitLab Maven Packages</name>
|
||||
<url>https://gitlab.com/api/v4/projects/68509751/packages/maven</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>${httpclient5.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>24.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit5.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>${mockito-junit5.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback-classic.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
<locale>en</locale>
|
||||
<source>${java.version}</source>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build javadoc jar</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<configuration>
|
||||
<tagNameFormat>v@{project.version}</tagNameFormat>
|
||||
<arguments>-DskipTests</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<!-- checkstyle -->
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${checkstyle.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle-validate</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>${checkstyle.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<checkstyle.version>12.3.0</checkstyle.version>
|
||||
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
|
||||
<checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml
|
||||
</checkstyle.config.location>
|
||||
<checkstyle.includeTestResources>false</checkstyle.includeTestResources>
|
||||
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
|
||||
<checkstyle.failOnViolation>false</checkstyle.failOnViolation>
|
||||
<checkstyle.propertyExpansion>config_loc=${basedir}
|
||||
</checkstyle.propertyExpansion>
|
||||
<checkstyle.consoleOutput>true</checkstyle.consoleOutput>
|
||||
<linkXRef>false</linkXRef>
|
||||
|
||||
<plugin>
|
||||
<!-- generates the code coverage report for sonar cube -->
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.12</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report-code-coverage</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<formats>XML</formats>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- 3rd party dependencies -->
|
||||
<jackson.version>2.21.1</jackson.version>
|
||||
<httpclient5.version>5.5.1</httpclient5.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<slf4j.version>2.0.17</slf4j.version>
|
||||
<logback-classic.version>1.5.25</logback-classic.version>
|
||||
<junit5.version>5.14.2</junit5.version>
|
||||
<mockito-junit5.version>5.21.0</mockito-junit5.version>
|
||||
|
||||
<plugin>
|
||||
<!-- to export test summary -->
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</plugin>
|
||||
<lombok-maven-plugin.version>1.18.20.0</lombok-maven-plugin.version>
|
||||
</properties>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
<scm>
|
||||
<developerConnection>scm:git:https://git.mein-gateway.de/thischwa/CloudflareDNS-java.git</developerConnection>
|
||||
<connection>scm:git:https://git.mein-gateway.de/thischwa/CloudflareDNS-java.git</connection>
|
||||
<url>https://git.mein-gateway.de/thischwa/CloudflareDNS-java</url>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>mygitea</id>
|
||||
<url>https://git.mein-gateway.de/api/packages/thischwa/maven</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>mygitea</id>
|
||||
<url>https://git.mein-gateway.de/api/packages/thischwa/maven</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>${httpclient5.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>24.0.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit5.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>${mockito-junit5.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback-classic.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.11.2</version>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
<failOnWarnings>false</failOnWarnings>
|
||||
<locale>en</locale>
|
||||
<source>${java.version}</source>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build javadoc jar</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<tagNameFormat>v@{project.version}</tagNameFormat>
|
||||
<arguments>-DskipTests</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<!-- checkstyle -->
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${checkstyle.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle-validate</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>${checkstyle.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<!-- generates the code coverage report for sonar cube -->
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.13</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report-code-coverage</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<formats>XML</formats>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<!-- to export test summary -->
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<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>
|
||||
<version>3.4.2</version>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Test Report
|
||||
|
||||
**✅ All tests passed**
|
||||
|
||||
## Test Results
|
||||
|
||||
| | Tests | Passed | Failed | Skipped |
|
||||
|---|---|---|---|---|
|
||||
| **Total** | 90 | 90 | 0 | 0 |
|
||||
|
||||
### Details
|
||||
|
||||
| Status | Suite | Tests | Passed | Failed | Skipped |
|
||||
|---|---|---|---|---|---|
|
||||
| ✅ | codes.thischwa.cf.CfBasicHttpClientTest | 9 | 9 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.CfClientPenTest | 0 | 0 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.CfClientTest | 0 | 0 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.CfDnsClientBuilderTest | 16 | 16 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.CfDnsClientMockTest | 16 | 16 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.CfRequestTest | 8 | 8 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.ObjectMapperTest | 2 | 2 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.ResponseValidatorTest | 8 | 8 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.fluent.FluentApiTest | 15 | 15 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.model.BatchEntryTest | 2 | 2 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.model.PagingRequestTest | 4 | 4 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.model.RecordEntityTest | 6 | 6 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.model.RecordTypeTest | 3 | 3 | 0 | 0 |
|
||||
| ✅ | codes.thischwa.cf.model.ZoneEntityTest | 1 | 1 | 0 | 0 |
|
||||
|
||||
## Coverage
|
||||
|
||||
| Package | Line Coverage | Branch Coverage |
|
||||
|---|---|---|
|
||||
| codes.thischwa.cf.fluent | 100% (25/25) | 100% (4/4) |
|
||||
| codes.thischwa.cf.model | 100% (93/93) | 90% (9/10) |
|
||||
| codes.thischwa.cf | 89% (249/279) | 71% (51/72) |
|
||||
@@ -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;
|
||||
@@ -21,6 +19,7 @@ import org.apache.hc.core5.http.HttpHeaders;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Abstract base class for creating HTTP clients to interact with the Cloudflare API. Provides
|
||||
@@ -29,23 +28,23 @@ import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
|
||||
*/
|
||||
@Slf4j
|
||||
abstract class CfBasicHttpClient {
|
||||
private final String baseUrl;
|
||||
private final String authEmail;
|
||||
private final String authKey;
|
||||
|
||||
private final String baseUrl;
|
||||
private final CfDnsClientBuilder.CfAuth auth;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
CfBasicHttpClient(String baseUrl, String authEmail, String authKey)
|
||||
throws IllegalArgumentException {
|
||||
if (authEmail == null || authEmail.isBlank()) {
|
||||
throw new IllegalArgumentException("Authentication email must not be null or blank!");
|
||||
}
|
||||
if (authKey == null || authKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Authentication key must not be null or blank!");
|
||||
}
|
||||
private record ResultWrapper(int statusCode, String responseBody) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Cloudflare HTTP client with the specified base URL and authentication.
|
||||
*
|
||||
* @param baseUrl the base URL for the Cloudflare API
|
||||
* @param auth the authentication mechanism to use
|
||||
*/
|
||||
CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfDnsClientBuilder.CfAuth auth) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.authEmail = authEmail;
|
||||
this.authKey = authKey;
|
||||
this.auth = auth;
|
||||
this.objectMapper = JsonConf.initObjectMapper();
|
||||
}
|
||||
|
||||
@@ -55,41 +54,53 @@ abstract class CfBasicHttpClient {
|
||||
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip");
|
||||
request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
|
||||
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
|
||||
request.addHeader("X-Auth-Email", authEmail);
|
||||
request.addHeader("X-Auth-Key", authKey);
|
||||
if (request instanceof ClassicHttpRequest classicRequest) {
|
||||
auth.applyAuth(classicRequest);
|
||||
}
|
||||
}).build();
|
||||
}
|
||||
|
||||
private <T extends AbstractResponse> T executeRequest(ClassicHttpRequest request,
|
||||
Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
throws CloudflareApiException {
|
||||
String logUri = null;
|
||||
try (CloseableHttpClient client = createHttpClient()) {
|
||||
ResultWrapper result = client.execute(request,
|
||||
(ClassicHttpResponse response) -> new ResultWrapper(response.getCode(),
|
||||
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
|
||||
(ClassicHttpResponse response) -> new ResultWrapper(response.getCode(),
|
||||
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
|
||||
|
||||
|
||||
T respObj = objectMapper.readValue(result.responseBody, responseType);
|
||||
if (!respObj.getResponseResultInfo().isSuccess()) {
|
||||
log.error("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) {
|
||||
T respObj = objectMapper.readValue(result.responseBody, responseType);
|
||||
if (!respObj.getResponseResultInfo().isSuccess()) {
|
||||
log.error("API error.");
|
||||
respObj.getResponseResultInfo().getErrors().forEach(e -> log.error(" - {}", e));
|
||||
throw new CloudflareApiException("API error.");
|
||||
}
|
||||
return respObj;
|
||||
} else {
|
||||
log.error("{} request failed for URL {}: Status {}", request.getMethod(), request.getUri(),
|
||||
result.statusCode);
|
||||
result.statusCode);
|
||||
throw new CloudflareApiException(
|
||||
request.getMethod() + " request failed with status code: " + result.statusCode);
|
||||
request.getMethod() + " request failed with status code: " + result.statusCode);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("JSON parsing error for request to {}", logUri, e);
|
||||
throw new CloudflareApiException("Error processing JSON response", e);
|
||||
} catch (Exception e) {
|
||||
throw new CloudflareApiException("Server error!", e);
|
||||
throw new CloudflareApiException("Unexpected error!", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,37 +108,45 @@ abstract class CfBasicHttpClient {
|
||||
* Sends a GET request to the given endpoint and maps the response.
|
||||
*/
|
||||
<T extends AbstractResponse> T getRequest(String endpoint, Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
throws CloudflareApiException {
|
||||
HttpGet request = new HttpGet(buildUrl(endpoint));
|
||||
return executeRequest(request, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
throws CloudflareApiException {
|
||||
<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,
|
||||
Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
<T extends AbstractResponse> T putRequest(String endpoint,
|
||||
Object requestPayload,
|
||||
Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
HttpPut request = new HttpPut(buildUrl(endpoint));
|
||||
setRequestPayload(request, requestPayload);
|
||||
return executeRequest(request, responseType);
|
||||
@@ -135,24 +154,34 @@ 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)
|
||||
throws CloudflareApiException {
|
||||
<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)
|
||||
throws CloudflareApiException {
|
||||
private void setRequestPayload(BasicClassicHttpRequest request,
|
||||
Object requestPayload)
|
||||
throws CloudflareApiException {
|
||||
try {
|
||||
request.setEntity(new StringEntity(objectMapper.writeValueAsString(requestPayload),
|
||||
ContentType.APPLICATION_JSON));
|
||||
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);
|
||||
}
|
||||
@@ -161,7 +190,4 @@ abstract class CfBasicHttpClient {
|
||||
private String buildUrl(String endpoint) {
|
||||
return baseUrl + endpoint;
|
||||
}
|
||||
|
||||
private record ResultWrapper(int statusCode, String responseBody) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,98 +12,128 @@ 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.HashSet;
|
||||
import java.util.List;
|
||||
import lombok.Setter;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
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
|
||||
* records and zones within the Cloudflare system, including creating, updating, retrieving, and
|
||||
* deleting DNS records.
|
||||
*
|
||||
* <p>Example:
|
||||
* <p>Example with API token authentication (recommended):
|
||||
* <pre><code>
|
||||
* // Create a new CfDnsClient instance
|
||||
* CfDnsClient cfDnsClient = new CfDnsClient(
|
||||
* "email@example.com",
|
||||
* "yourApiKey"
|
||||
* );
|
||||
* // Create a new CfDnsClient instance with API token
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withApiTokenAuth("your-api-token")
|
||||
* .build();
|
||||
*
|
||||
* // 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<{@link 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())
|
||||
* );
|
||||
*
|
||||
* // 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>
|
||||
*
|
||||
* <p>Example with email/key authentication (legacy):
|
||||
* <pre><code>
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withEmailKeyAuth("email@example.com", "your-api-key")
|
||||
* .build();
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>Example with exception throwing enabled:
|
||||
* <pre><code>
|
||||
* // Throws exception when results are empty
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withApiTokenAuth("your-api-token")
|
||||
* .withEmptyResultThrowsException(true)
|
||||
* .build();
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>Example with custom base URL:
|
||||
* <pre><code>
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withApiTokenAuth("your-api-token")
|
||||
* .withBaseUrl("https://custom-api.example.com")
|
||||
* .build();
|
||||
* </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;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@code CfDnsClient}.
|
||||
*
|
||||
* @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 authentication
|
||||
* process.
|
||||
*/
|
||||
public CfDnsClient(String authEmail, String authKey) {
|
||||
this(DEFAULT_BASEURL, authEmail, authKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@code CfDnsClient}.
|
||||
*
|
||||
* @param baseUrl The base URL of the Cloudflare API to be used for requests.
|
||||
* @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 authentication
|
||||
* process.
|
||||
*/
|
||||
public CfDnsClient(String baseUrl, String authEmail, String authKey) {
|
||||
this(true, baseUrl, authEmail, authKey);
|
||||
}
|
||||
private final boolean emptyResultThrowsException;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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
|
||||
* authentication process.
|
||||
*/
|
||||
public CfDnsClient(boolean emptyResultThrowsException, String authEmail, String authKey) {
|
||||
this(emptyResultThrowsException, DEFAULT_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 baseUrl The base URL for the Cloudflare API endpoint.
|
||||
* @param authEmail The email associated with the Cloudflare account for
|
||||
* authentication.
|
||||
* @param authKey The API key for authenticating the client with Cloudflare
|
||||
* services.
|
||||
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
|
||||
*/
|
||||
public CfDnsClient(boolean emptyResultThrowsException, String baseUrl, String authEmail,
|
||||
String authKey) {
|
||||
super(baseUrl, authEmail, authKey);
|
||||
CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfDnsClientBuilder.CfAuth auth) {
|
||||
super(baseUrl, auth);
|
||||
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 in 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")
|
||||
* .getRecord("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 +142,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 +155,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 +170,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,155 +178,184 @@ public class CfDnsClient extends CfBasicHttpClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all record entities for a specific second-level domain (SLD) within a given DNS
|
||||
* zone.
|
||||
* Retrieves a list of DNS records for a specified zone, with optional paging support.
|
||||
*
|
||||
* @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 containing information about the target zone.
|
||||
* @return A list of RecordEntity objects representing the DNS records of the specified zone.
|
||||
* @throws CloudflareApiException If an error occurs during the API request or response processing.
|
||||
*/
|
||||
public List<RecordEntity> sldListAll(ZoneEntity zone, String sld) throws CloudflareApiException {
|
||||
return sldListAll(zone, sld, PagingRequest.defaultPaging());
|
||||
public List<RecordEntity> recordList(ZoneEntity zone) throws CloudflareApiException {
|
||||
return recordList(zone, (PagingRequest) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all record entities for a specific second-level domain (SLD) within a given DNS
|
||||
* zone.
|
||||
* Retrieves a list of DNS records for a specified zone, with optional paging support.
|
||||
*
|
||||
* @param zone The DNS zone entity for which the SLD records are to be fetched.
|
||||
* @param sld The second-level domain name for which the records are retrieved.
|
||||
* @param pagingRequest The paging request.
|
||||
* @return A list of {@code RecordEntity} associated with the desired SLD.
|
||||
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
|
||||
* @param zone The zone entity containing information about the target zone.
|
||||
* @param pagingRequest The paging request containing parameters such as page size and number.
|
||||
* @return A list of RecordEntity objects representing the DNS records of the specified zone.
|
||||
* @throws CloudflareApiException If an error occurs during the API request or response processing.
|
||||
*/
|
||||
public List<RecordEntity> sldListAll(ZoneEntity zone, String sld, PagingRequest pagingRequest)
|
||||
throws CloudflareApiException {
|
||||
String fqdn = buildFqdn(zone, sld);
|
||||
String endpoint =
|
||||
pagingRequest.addQueryString(CfRequest.RECORD_INFO_NAME.buildPath(zone.getId(), fqdn));
|
||||
public List<RecordEntity> recordList(ZoneEntity zone, @Nullable PagingRequest pagingRequest) throws CloudflareApiException {
|
||||
PagingRequest pr = pagingRequest == null ? PagingRequest.defaultPaging() : pagingRequest;
|
||||
String endpoint = pr.addQueryString(CfRequest.RECORD_LIST.buildPath(zone.getId()));
|
||||
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
|
||||
checkResponse(resp);
|
||||
return resp.getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves detailed information about a specific second-level domain (SLD) record for a given
|
||||
* zone and record type from the Cloudflare API.
|
||||
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
|
||||
*
|
||||
* @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 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 RecordEntity sldInfo(ZoneEntity zone, String sld, RecordType type)
|
||||
throws CloudflareApiException {
|
||||
String fqdn = buildFqdn(zone, sld);
|
||||
String endpoint = CfRequest.RECORD_INFO_NAME_TYPE.buildPath(zone.getId(), fqdn, type);
|
||||
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
|
||||
checkResponse(resp, true);
|
||||
return resp.getResult().get(0);
|
||||
public List<RecordEntity> recordList(ZoneEntity zone, String sld) throws CloudflareApiException {
|
||||
return recordList(zone, sld, (RecordType[]) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DNS record for a given second-level domain (SLD) within the specified zone.
|
||||
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
|
||||
* Optionally, filters by one or more DNS getRecord types.
|
||||
*
|
||||
* @param zone The ZoneEntity representing the DNS zone where the record is to be created.
|
||||
* @param sld The second-level domain (SLD) for which the DNS record is being created.
|
||||
* @param ttl The time-to-live (TTL) value for the DNS record in seconds.
|
||||
* @param type The RecordType specifying the type of the DNS record (e.g., A, AAAA, CNAME).
|
||||
* @param content The content of the DNS record (e.g., IP address for A/AAAA records, target
|
||||
* @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 getRecord 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 {
|
||||
String fqdn = buildFqdn(zone, sld);
|
||||
String endpoint = CfRequest.RECORD_LIST_NAME.buildPath(zone.getId(), fqdn);
|
||||
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
|
||||
checkResponse(resp, false);
|
||||
List<RecordEntity> recs = resp.getResult();
|
||||
return filterAndSetZoneRecords(zone, types, recs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of all DNS records for a given zone.
|
||||
* Optionally, filters by one or more DNS getRecord types.
|
||||
*
|
||||
* @param zone The zone entity containing information about the domain zone.
|
||||
* @param types Optional parameter specifying one or more DNS getRecord types to filter the results.
|
||||
* @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 List<RecordEntity> recordList(ZoneEntity zone, RecordType... types)
|
||||
throws CloudflareApiException {
|
||||
String endpoint = CfRequest.RECORD_LIST.buildPath(zone.getId());
|
||||
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
|
||||
checkResponse(resp, false);
|
||||
List<RecordEntity> recs = resp.getResult();
|
||||
return filterAndSetZoneRecords(zone, types, recs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DNS getRecord for a given second-level domain (SLD) within the specified zone.
|
||||
*
|
||||
* @param zone The ZoneEntity representing the DNS zone where the getRecord is to be created.
|
||||
* @param sld The second-level domain (SLD) for which the DNS getRecord is being created.
|
||||
* @param ttl The time-to-live (TTL) value for the DNS getRecord in seconds.
|
||||
* @param type The RecordType specifying the type of the DNS getRecord (e.g., A, AAAA, CNAME).
|
||||
* @param content The content of the DNS getRecord (e.g., IP address for A/AAAA records, target
|
||||
* domain for CNAME).
|
||||
* @return The created RecordEntity object containing details of the newly created DNS record.
|
||||
* @return The created RecordEntity object containing details of the newly created DNS getRecord.
|
||||
* @throws CloudflareApiException If an error occurs while communicating with the Cloudflare API
|
||||
* or creating the record.
|
||||
* or creating the getRecord.
|
||||
*/
|
||||
public RecordEntity recordCreateSld(ZoneEntity zone, String sld, int ttl, RecordType type,
|
||||
String content) throws CloudflareApiException {
|
||||
String content) throws CloudflareApiException {
|
||||
String fqdn = buildFqdn(zone, sld);
|
||||
return recordCreate(zone, fqdn, ttl, type, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DNS record in the specified DNS zone with the provided details.
|
||||
* Creates a DNS getRecord in the specified DNS zone with the provided details.
|
||||
*
|
||||
* @param zone the DNS zone in which the record will be created
|
||||
* @param name the name of the DNS record (e.g., www.example.com)
|
||||
* @param ttl the time-to-live (TTL) value for the DNS record
|
||||
* @param type the type of the DNS record (e.g., A, AAAA, CNAME)
|
||||
* @param content the content or value of the DNS record
|
||||
* @return the created DNS record as a {@link RecordEntity} object
|
||||
* @param zone the DNS zone in which the getRecord will be created
|
||||
* @param name the name of the DNS getRecord (e.g., www.example.com)
|
||||
* @param ttl the time-to-live (TTL) value for the DNS getRecord
|
||||
* @param type the type of the DNS getRecord (e.g., A, AAAA, CNAME)
|
||||
* @param content the content or value of the DNS getRecord
|
||||
* @return the created DNS getRecord as a {@link RecordEntity} object
|
||||
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
|
||||
*/
|
||||
public RecordEntity recordCreate(ZoneEntity zone, String name, int ttl, RecordType type,
|
||||
String content) throws CloudflareApiException {
|
||||
String content) throws CloudflareApiException {
|
||||
RecordEntity rec = RecordEntity.build(name, type, ttl, content);
|
||||
return recordCreate(zone, rec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DNS record in the specified zone using the Cloudflare API.
|
||||
* Creates a new DNS getRecord in the specified zone using the Cloudflare API.
|
||||
*
|
||||
* @param zone The zone entity where the record will be created. Contains details such as zone
|
||||
* @param zone The zone entity where the getRecord will be created. Contains details such as zone
|
||||
* ID.
|
||||
* @param rec The record entity representing the DNS record to be created, including its
|
||||
* @param rec The getRecord entity representing the DNS getRecord to be created, including its
|
||||
* attributes.
|
||||
* @return The created record entity as returned by the Cloudflare API.
|
||||
* @return The created getRecord entity as returned by the Cloudflare API.
|
||||
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
|
||||
*/
|
||||
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 retRec = resp.getResult();
|
||||
retRec.setZoneId(zone.getId());
|
||||
return retRec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a DNS record of the specified type within a given zone on the Cloudflare API.
|
||||
* Deletes a DNS getRecord of the specified type within a given zone on the Cloudflare API.
|
||||
*
|
||||
* @param zone The zone entity that specifies the zone in which the record exists.
|
||||
* @param rec The record entity that represents the DNS record to be deleted.
|
||||
* @return {@code true} if the DNS record was successfully deleted; {@code false} otherwise.
|
||||
* @param zone The zone entity that specifies the zone in which the getRecord exists.
|
||||
* @param rec The getRecord entity that represents the DNS getRecord to be deleted.
|
||||
* @return {@code true} if the DNS getRecord was successfully deleted; {@code false} otherwise.
|
||||
* @throws CloudflareApiException if there is an issue during the API communication, or the
|
||||
* request fails for any reason.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a DNS record of the specified type within a given zone on the Cloudflare API.
|
||||
* Deletes a DNS getRecord of the specified type within a given zone on the Cloudflare API.
|
||||
*
|
||||
* @param zone The zone entity that specifies the zone in which the record exists.
|
||||
* @param id The record entity that represents the DNS record to be deleted.
|
||||
* @return {@code true} if the DNS record was successfully deleted; {@code false} otherwise.
|
||||
* @param zone The zone entity that specifies the zone in which the getRecord exists.
|
||||
* @param id The getRecord entity that represents the DNS getRecord to be deleted.
|
||||
* @return {@code true} if the DNS getRecord was successfully deleted; {@code false} otherwise.
|
||||
* @throws CloudflareApiException if there is an issue during the API communication or the request
|
||||
* fails for any reason.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing DNS record in a specified Cloudflare zone.
|
||||
* Updates an existing DNS getRecord in a specified Cloudflare zone.
|
||||
*
|
||||
* @param zone the zone entity containing the ID of the target zone
|
||||
* @param rec the record entity containing the ID of the DNS record to be updated and its updated
|
||||
* @param rec the getRecord entity containing the ID of the DNS getRecord to be updated and its updated
|
||||
* data
|
||||
* @return the updated record entity as returned by the Cloudflare API
|
||||
* @return the updated getRecord entity as returned by the Cloudflare API
|
||||
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
|
||||
*/
|
||||
public RecordEntity recordUpdate(ZoneEntity zone, RecordEntity rec)
|
||||
@@ -301,37 +364,132 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes DNS records of a specific type within a given zone if they exist. If no record of the
|
||||
* Deletes DNS records of a specific type within a given zone if they exist. If no getRecord of the
|
||||
* specified type exists, it logs this occurrence without throwing an exception.
|
||||
*
|
||||
* @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 zone The DNS zone entity in which the getRecord exists.
|
||||
* @param sld The second-level domain for which the getRecord is being checked.
|
||||
* @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 getRecord 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 getRecord {} 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 getRecord 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' getRecord 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) {
|
||||
Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types));
|
||||
filtered = recs.stream()
|
||||
.filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType()))).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 {
|
||||
@@ -342,4 +500,5 @@ public class CfDnsClient extends CfBasicHttpClient {
|
||||
throws CloudflareApiException {
|
||||
responseValidator.validate(resp, singleResultExpected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package codes.thischwa.cf;
|
||||
|
||||
import org.apache.hc.core5.http.ClassicHttpRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Builder class for configuring and creating instances of {@link CfDnsClient}.
|
||||
* This class provides a fluent API for customizing the client settings,
|
||||
* such as the base URL and authentication mechanism.
|
||||
*/
|
||||
public class CfDnsClientBuilder {
|
||||
|
||||
/**
|
||||
* The default base URL for the Cloudflare v4 API requests made by the {@code CfDnsClient}.
|
||||
*/
|
||||
public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
private boolean emptyResultThrowsException;
|
||||
private CfAuth auth;
|
||||
|
||||
@Nullable
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* Configures whether an exception should be thrown when an empty result is encountered
|
||||
* during operations performed by the `CfDnsClient`.
|
||||
*
|
||||
* @param emptyResultThrowsException a boolean flag indicating if an exception should be thrown
|
||||
* when an empty result is returned. If set to `true`, operations
|
||||
* that result in an empty response will throw an exception;
|
||||
* otherwise, they will not.
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
|
||||
* to further configure the builder.
|
||||
*/
|
||||
public CfDnsClientBuilder withEmptyResultThrowsException(boolean emptyResultThrowsException) {
|
||||
this.emptyResultThrowsException = emptyResultThrowsException;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL to be used by the {@code CfDnsClient}.
|
||||
* This method allows configuring the base URL for API requests, overriding any default value.
|
||||
*
|
||||
* @param baseUrl the base URL to be used for API requests
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, enabling method chaining
|
||||
*/
|
||||
public CfDnsClientBuilder withBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the authentication method for the {@code CfDnsClient} to use an API token.
|
||||
* This is the recommended way to authenticate with the Cloudflare API, as it provides
|
||||
* enhanced security and ease of use compared to other authentication mechanisms.
|
||||
*
|
||||
* @param apiToken the Cloudflare API token. This token is required for authenticating
|
||||
* API requests and must not be null or blank.
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
|
||||
* to further configure the builder.
|
||||
* @throws IllegalArgumentException if the {@code apiToken} is null or blank.
|
||||
*/
|
||||
public CfDnsClientBuilder withApiTokenAuth(String apiToken) {
|
||||
this.auth = new ApiTokenAuth(apiToken);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the authentication method for the {@code CfDnsClient} to use an email and API key.
|
||||
* This approach uses the legacy authentication mechanism provided by Cloudflare, where requests
|
||||
* are authenticated with a combination of an account's email address and API key.
|
||||
*
|
||||
* @param authEmail the email address associated with the Cloudflare account. This must not be null or blank.
|
||||
* @param authKey the API key of the Cloudflare account. This must not be null or blank.
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
|
||||
* to further configure the builder.
|
||||
* @throws IllegalArgumentException if {@code authEmail} or {@code authKey} is null or blank.
|
||||
*/
|
||||
public CfDnsClientBuilder withEmailKeyAuth(String authEmail, String authKey) {
|
||||
this.auth = new EmailKeyAuth(authEmail, authKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a configured instance of {@code CfDnsClient}.
|
||||
*
|
||||
* <p>The method constructs a new {@code CfDnsClient} object based on the
|
||||
* options set in the {@code CfDnsClientBuilder}. If no base URL has been
|
||||
* explicitly configured, a default base URL will be used.
|
||||
*
|
||||
* @return a new instance of {@code CfDnsClient} configured with the
|
||||
* specified options such as base URL, authentication details,
|
||||
* and the exception-handling policy for empty results.
|
||||
*/
|
||||
public CfDnsClient build() {
|
||||
String url = baseUrl == null ? DEFAULT_BASEURL : baseUrl;
|
||||
return new CfDnsClient(emptyResultThrowsException, url, auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Cloudflare authentication mechanisms.
|
||||
* Implementations of this interface provide different methods of authentication
|
||||
* with the Cloudflare API (e.g., API token, email/key combination).
|
||||
*/
|
||||
interface CfAuth {
|
||||
|
||||
/**
|
||||
* Applies authentication headers to the given HTTP request.
|
||||
*
|
||||
* @param request the HTTP request to authenticate
|
||||
*/
|
||||
void applyAuth(ClassicHttpRequest request);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication mechanism using Cloudflare API token.
|
||||
* This is the recommended authentication method for the Cloudflare API.
|
||||
*/
|
||||
static class ApiTokenAuth implements CfAuth {
|
||||
|
||||
private final String apiToken;
|
||||
|
||||
/**
|
||||
* Creates a new API token authentication object.
|
||||
*
|
||||
* @param apiToken the Cloudflare API token
|
||||
* @throws IllegalArgumentException if the API token is null or blank
|
||||
*/
|
||||
ApiTokenAuth(@NotNull String apiToken) {
|
||||
if (apiToken.isBlank()) {
|
||||
throw new IllegalArgumentException("API token must not be null or blank!");
|
||||
}
|
||||
this.apiToken = apiToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyAuth(ClassicHttpRequest request) {
|
||||
request.addHeader("Authorization", "Bearer " + apiToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication mechanism using Cloudflare account email and API key.
|
||||
* This is the legacy authentication method for the Cloudflare API.
|
||||
*/
|
||||
static class EmailKeyAuth implements CfAuth {
|
||||
|
||||
private final String authEmail;
|
||||
private final String authKey;
|
||||
|
||||
/**
|
||||
* Creates a new email/key authentication object.
|
||||
*
|
||||
* @param authEmail the email address associated with the Cloudflare account
|
||||
* @param authKey the API key of the Cloudflare account
|
||||
* @throws IllegalArgumentException if email or key is null or blank
|
||||
*/
|
||||
EmailKeyAuth(@NotNull String authEmail, @NotNull String authKey) {
|
||||
if (authEmail.isBlank()) {
|
||||
throw new IllegalArgumentException("Authentication email must not be null or blank!");
|
||||
}
|
||||
if (authKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Authentication key must not be null or blank!");
|
||||
}
|
||||
this.authEmail = authEmail;
|
||||
this.authKey = authKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyAuth(ClassicHttpRequest request) {
|
||||
request.addHeader("X-Auth-Email", authEmail);
|
||||
request.addHeader("X-Auth-Key", authKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -19,33 +21,38 @@ public enum CfRequest {
|
||||
ZONE_INFO("/zones?name=%s"),
|
||||
|
||||
/**
|
||||
* Represents the API endpoint path for creating a new DNS record within a specific DNS zone. The
|
||||
* 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 retrieving information about a DNS getRecord within a
|
||||
* specific DNS zone by its name. The endpoint path includes placeholders for the zone identifier
|
||||
* and the getRecord name, which need to be provided to construct the complete path.
|
||||
*/
|
||||
RECORD_LIST_NAME("/zones/%s/dns_records?name=%s"),
|
||||
/**
|
||||
* Represents the API endpoint path for creating a new DNS getRecord within a specific DNS zone. The
|
||||
* endpoint path includes a placeholder for the zone identifier, which needs to be provided to
|
||||
* construct the complete path.
|
||||
*/
|
||||
RECORD_CREATE("/zones/%s/dns_records"),
|
||||
/**
|
||||
* Represents the API endpoint path for retrieving information about a DNS record within a
|
||||
* specific DNS zone by its name. The endpoint path includes placeholders for the zone identifier
|
||||
* 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
|
||||
* Represents the API endpoint path for updating an existing DNS getRecord within a specific DNS
|
||||
* zone. The endpoint path includes placeholders for the zone identifier and the getRecord
|
||||
* identifier, which need to be provided to construct the complete path.
|
||||
*/
|
||||
RECORD_UPDATE("/zones/%s/dns_records/%s"),
|
||||
/**
|
||||
* 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
|
||||
* 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 getRecord within a specific DNS
|
||||
* zone. The endpoint path includes placeholders for the zone identifier and the getRecord
|
||||
* identifier, which need to be provided to construct the complete path.
|
||||
*/
|
||||
RECORD_DELETE("/zones/%s/dns_records/%s");
|
||||
@@ -62,7 +69,7 @@ public enum CfRequest {
|
||||
* arguments.
|
||||
*
|
||||
* @param vars the arguments to format the path string with; these are typically specific
|
||||
* identifiers or parameters required by the API endpoint.
|
||||
* identifiers or parameters required by the API endpoint.
|
||||
* @return the fully constructed API endpoint path as a string.
|
||||
*/
|
||||
String buildPath(Object... vars) {
|
||||
|
||||
@@ -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.
|
||||
@@ -22,9 +23,9 @@ public class CloudflareApiException extends Exception {
|
||||
* Constructs a new CloudflareApiException with the specified detail message and cause.
|
||||
*
|
||||
* @param message the detail message, which provides additional context or information about the
|
||||
* exception.
|
||||
* @param cause the cause of this exception, which is the underlying throwable that triggered this
|
||||
* exception.
|
||||
* exception.
|
||||
* @param cause the cause of this exception, which is the underlying throwable that triggered this
|
||||
* exception.
|
||||
*/
|
||||
public CloudflareApiException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
|
||||
@@ -13,22 +13,9 @@ public class CloudflareNotFoundException extends CloudflareApiException {
|
||||
* Constructs a new CloudflareNotFoundException with the specified detail message.
|
||||
*
|
||||
* @param message the detail message, which provides additional context about the "not found"
|
||||
* error encountered during interaction with the Cloudflare API.
|
||||
* error encountered during interaction with the Cloudflare API.
|
||||
*/
|
||||
public CloudflareNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CloudflareNotFoundException with the specified detail message and cause.
|
||||
*
|
||||
* @param message the detail message, which provides additional context about the "not found"
|
||||
* error encountered during interaction with the Cloudflare API.
|
||||
* @param cause the cause of this exception, which is the underlying throwable that triggered this
|
||||
* exception.
|
||||
*/
|
||||
public CloudflareNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class JsonConf {
|
||||
static ObjectMapper initObjectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
|
||||
return mapper;
|
||||
|
||||
@@ -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 {
|
||||
@@ -43,13 +46,27 @@ class ResponseValidator {
|
||||
private void validateResultCount(AbstractResponse resp, boolean singleResultExpected)
|
||||
throws CloudflareApiException {
|
||||
if (resp instanceof RecordMultipleResponse respMulti) {
|
||||
if (singleResultExpected && respMulti.getResultInfo().totalCount() > 1) {
|
||||
throw new CloudflareApiException(
|
||||
"Unexpected result count: " + respMulti.getResultInfo().totalCount());
|
||||
}
|
||||
if (emptyResultThrowsException && respMulti.getResultInfo().totalCount() == 0) {
|
||||
throw new CloudflareNotFoundException("No result found");
|
||||
}
|
||||
validateMultipleResponse(respMulti, singleResultExpected);
|
||||
} else if (resp instanceof AbstractSingleResponse<?> respSingle) {
|
||||
validateSingleResponse(respSingle);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMultipleResponse(RecordMultipleResponse response, boolean singleResultExpected)
|
||||
throws CloudflareApiException {
|
||||
int totalCount = response.getResultInfo().totalCount();
|
||||
if (singleResultExpected && totalCount > 1) {
|
||||
throw new CloudflareApiException("Unexpected result count: " + totalCount);
|
||||
}
|
||||
if (emptyResultThrowsException && totalCount == 0) {
|
||||
throw new CloudflareNotFoundException("No result found");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSingleResponse(AbstractSingleResponse<?> response)
|
||||
throws CloudflareNotFoundException {
|
||||
if (emptyResultThrowsException && response.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 getRecord-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 getRecord with the specified parameters.
|
||||
*
|
||||
* @param type the DNS getRecord type (e.g., A, AAAA, CNAME)
|
||||
* @param content the content of the DNS getRecord (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 getRecord
|
||||
*/
|
||||
RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException;
|
||||
|
||||
/**
|
||||
* Updates an existing DNS getRecord with new content.
|
||||
*
|
||||
* @param newContent the new content for the DNS getRecord
|
||||
* @return the updated RecordEntity
|
||||
* @throws CloudflareApiException if an error occurs while updating the getRecord
|
||||
*/
|
||||
RecordEntity update(String newContent) throws CloudflareApiException;
|
||||
|
||||
/**
|
||||
* Deletes DNS records of the specified types.
|
||||
*
|
||||
* @param types the DNS getRecord 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 getRecord-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> recs = get();
|
||||
if (recs.isEmpty()) {
|
||||
throw new CloudflareApiException("No recs found to update for subdomain: " + sld);
|
||||
}
|
||||
if (recs.size() > 1) {
|
||||
throw new CloudflareApiException("Multiple recs found. Please use recordUpdate() directly for precise control.");
|
||||
}
|
||||
RecordEntity rec = recs.get(0);
|
||||
rec.setContent(newContent);
|
||||
return client.recordUpdate(zone, rec);
|
||||
}
|
||||
|
||||
@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 getRecord(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 getRecord(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 getRecord(String sld) throws CloudflareApiException {
|
||||
return new RecordOperationsImpl(client, zone, sld, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecordOperations getRecord(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 getRecord
|
||||
* client.zone("example.com")
|
||||
* .getRecord("api")
|
||||
* .create(RecordType.A, "192.168.1.1", 60);
|
||||
*
|
||||
* // Get DNS records
|
||||
* List<RecordEntity> records = client.zone("example.com")
|
||||
* .getRecord("www", RecordType.A)
|
||||
* .get();
|
||||
*
|
||||
* // Update a DNS getRecord
|
||||
* client.zone("example.com")
|
||||
* .getRecord("api", RecordType.A)
|
||||
* .update("192.168.1.2");
|
||||
*
|
||||
* // Delete DNS records
|
||||
* client.zone("example.com")
|
||||
* .getRecord("old-service")
|
||||
* .delete(RecordType.A, RecordType.AAAA);
|
||||
* </code></pre>
|
||||
*/
|
||||
|
||||
package codes.thischwa.cf.fluent;
|
||||
@@ -24,7 +24,7 @@ import lombok.EqualsAndHashCode;
|
||||
* <p>Subclasses can be created by specifying the entity type that the response should handle.
|
||||
*
|
||||
* @param <T> Represents the type of entities contained within the response. For this class, it is
|
||||
* expected to be {@code ResponseEntity}.
|
||||
* expected to be {@code ResponseEntity}.
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
|
||||
@@ -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 getRecord 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 getRecord 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 getRecord 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 = 1000;
|
||||
|
||||
private int page;
|
||||
private int perPage;
|
||||
|
||||
@@ -32,7 +38,7 @@ public class PagingRequest {
|
||||
/**
|
||||
* Creates a new {@code PagingRequest} instance with the specified page number and items per page.
|
||||
*
|
||||
* @param page the page number to be requested
|
||||
* @param page the page number to be requested
|
||||
* @param perPage the number of items to be included per page
|
||||
* @return a new {@code PagingRequest} instance with the provided parameters
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +77,7 @@ public class PagingRequest {
|
||||
}
|
||||
|
||||
private String queryString(boolean add) {
|
||||
String qs = "page=" + page + "&perPage=" + perPage;
|
||||
String qs = "page=" + page + "&per_page=" + perPage;
|
||||
return add ? "&" + qs : "?" + qs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,22 @@ import lombok.EqualsAndHashCode;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Represents a DNS record entity within a specific zone.
|
||||
* Represents a DNS getRecord entity within a specific zone.
|
||||
*
|
||||
* <p>Attributes defined in this class include:
|
||||
*
|
||||
* <ul>
|
||||
* <li>DNS record type such as "A" or "CNAME".
|
||||
* <li>Name of the DNS record.
|
||||
* <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>DNS getRecord type such as "A" or "CNAME".
|
||||
* <li>Name of the DNS getRecord.
|
||||
* <li>Content of the DNS getRecord, such as an IP address.
|
||||
* <li>Flags indicating whether the getRecord is proxiable or proxied.
|
||||
* <li>TTL (Time-To-Live) for the DNS getRecord.
|
||||
* <li>A locked status to indicate the immutability of the getRecord.
|
||||
* <li>Zone-specific metadata including zone ID and name.
|
||||
* <li>Timestamps for creation and modification.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Provides a static factory method {@code build} for creating a DNS record with specific
|
||||
* <p>Provides a static factory method {@code build} for creating a DNS getRecord with specific
|
||||
* attributes.
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@@ -34,14 +34,18 @@ 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
|
||||
* the AbstractEntity class. The RecordEntity class represents a DNS record entity within a
|
||||
* the AbstractEntity class. The RecordEntity class represents a DNS getRecord entity within a
|
||||
* specific zone, encapsulating attributes such as type, name, content, TTL, and other related
|
||||
* metadata.
|
||||
*/
|
||||
@@ -52,18 +56,87 @@ public class RecordEntity extends AbstractEntity {
|
||||
/**
|
||||
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
|
||||
*
|
||||
* @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 name the name of the DNS getRecord
|
||||
* @param type the {@link RecordType} of the DNS getRecord
|
||||
* @param ttl the time-to-live (TTL) value for the DNS getRecord
|
||||
* @param content the content of the DNS getRecord, 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.name = name;
|
||||
rec.type = type.getType();
|
||||
rec.ttl = ttl;
|
||||
rec.content = content;
|
||||
return rec;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a {@link RecordEntity} instance with the specified ID and content.
|
||||
*
|
||||
* @param id the unique identifier for the DNS getRecord
|
||||
* @param content the content of the DNS getRecord, typically an IP address or other getRecord 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.content = content;
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
|
||||
*
|
||||
* @param id the unique identifier for the DNS getRecord
|
||||
* @param name the name of the DNS getRecord
|
||||
* @param type the type of the DNS getRecord, represented as a string (e.g., "A", "CNAME")
|
||||
* @param ttl the time-to-live (TTL) value for the DNS getRecord
|
||||
* @param content the content of the DNS getRecord, typically an IP address or other getRecord 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 getRecord type: " + type + ". Must be one of: "
|
||||
+ java.util.Arrays.toString(RecordType.values()), e);
|
||||
}
|
||||
RecordEntity rec = new RecordEntity();
|
||||
rec.setId(id);
|
||||
rec.name = name;
|
||||
rec.type = recordType.getType();
|
||||
rec.ttl = ttl;
|
||||
rec.content = content;
|
||||
return rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the short name (subdomain) of the DNS getRecord.
|
||||
* 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 getRecord (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;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public class RecordMultipleResponse extends AbstractMultipleResponse<RecordEntit
|
||||
/**
|
||||
* Constructs an instance of RecordMultipleResponse.
|
||||
*
|
||||
* <p>This class represents a response containing multiple DNS record entities from the
|
||||
* <p>This class represents a response containing multiple DNS getRecord entities from the
|
||||
* Cloudflare API. It inherits functionality from AbstractMultipleResponse to handle multiple
|
||||
* records of type RecordEntity.
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ public class RecordSingleResponse extends AbstractSingleResponse<RecordEntity> {
|
||||
*
|
||||
* <p>This constructor initializes the RecordSingleResponse object by invoking the superclass
|
||||
* constructor. The RecordSingleResponse represents a specific API response structure that
|
||||
* encapsulates a single DNS record entity, providing mechanisms to interact with such data in the
|
||||
* encapsulates a single DNS getRecord entity, providing mechanisms to interact with such data in the
|
||||
* context of the Cloudflare API.
|
||||
*/
|
||||
public RecordSingleResponse() {
|
||||
|
||||
@@ -3,86 +3,86 @@ package codes.thischwa.cf.model;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Enum representing various DNS record types.
|
||||
* Enum representing various DNS getRecord types.
|
||||
*
|
||||
* <p>Each constant in this enum corresponds to a specific DNS record type, such as "A", "AAAA",
|
||||
* "CNAME", or "TXT". This enum provides a means to standardize the representation of these record
|
||||
* <p>Each constant in this enum corresponds to a specific DNS getRecord type, such as "A", "AAAA",
|
||||
* "CNAME", or "TXT". This enum provides a means to standardize the representation of these getRecord
|
||||
* types throughout the application while allowing easy retrieval of their string representation.
|
||||
*/
|
||||
@Getter
|
||||
public enum RecordType {
|
||||
/**
|
||||
* Represents the DNS A record type.
|
||||
* Represents the DNS A getRecord type.
|
||||
*
|
||||
* <p>The "A" record type is used to map a domain name to an IPv4 address.
|
||||
* <p>The "A" getRecord type is used to map a domain name to an IPv4 address.
|
||||
*/
|
||||
A("A"),
|
||||
/**
|
||||
* Represents the DNS AAAA record type.
|
||||
* Represents the DNS AAAA getRecord type.
|
||||
*
|
||||
* <p>The "AAAA" record type maps a domain name to an IPv6 address.
|
||||
* <p>The "AAAA" getRecord type maps a domain name to an IPv6 address.
|
||||
*/
|
||||
AAAA("AAAA"),
|
||||
/**
|
||||
* Represents the DNS CAA (Certificate Authority Authorization) record type.
|
||||
* Represents the DNS CAA (Certificate Authority Authorization) getRecord type.
|
||||
*
|
||||
* <p>The "CAA" record type is used to specify which certificate authorities (CAs) are allowed to
|
||||
* <p>The "CAA" getRecord type is used to specify which certificate authorities (CAs) are allowed to
|
||||
* issue SSL/TLS certificates for a domain.
|
||||
*/
|
||||
CAA("CAA"),
|
||||
/**
|
||||
* Represents the DNS CERT record type.
|
||||
* Represents the DNS CERT getRecord type.
|
||||
*
|
||||
* <p>The "CERT" record type is used to store certificates and related certificate revocation
|
||||
* <p>The "CERT" getRecord type is used to store certificates and related certificate revocation
|
||||
* lists or certificate authority data in DNS zones.
|
||||
*/
|
||||
CERT("CERT"),
|
||||
/**
|
||||
* Represents the DNS CNAME (Canonical Name) record type.
|
||||
* Represents the DNS CNAME (Canonical Name) getRecord type.
|
||||
*
|
||||
* <p>The "CNAME" record type is used to alias one domain name to another.
|
||||
* <p>The "CNAME" getRecord type is used to alias one domain name to another.
|
||||
*/
|
||||
CNAME("CNAME"),
|
||||
/**
|
||||
* Represents the DNSKEY record type.
|
||||
* Represents the DNSKEY getRecord type.
|
||||
*
|
||||
* <p>The "DNSKEY" record type is used for storing public keys in DNS, as part of the DNS Security
|
||||
* <p>The "DNSKEY" getRecord type is used for storing public keys in DNS, as part of the DNS Security
|
||||
* Extensions (DNSSEC). It helps in verifying the authenticity of DNS responses through digital
|
||||
* signatures.
|
||||
*/
|
||||
DNSKEY("DNSKEY"),
|
||||
/**
|
||||
* Represents the DNS DS (Delegation Signer) record type.
|
||||
* Represents the DNS DS (Delegation Signer) getRecord type.
|
||||
*
|
||||
* <p>The "DS" record type is used in the DNSSEC (Domain Name System Security Extensions)
|
||||
* protocol. It contains a hash of a DNSKEY record which is utilized in establishing a chain of
|
||||
* <p>The "DS" getRecord type is used in the DNSSEC (Domain Name System Security Extensions)
|
||||
* protocol. It contains a hash of a DNSKEY getRecord which is utilized in establishing a chain of
|
||||
* trust from a parent zone to a child zone.
|
||||
*/
|
||||
DS("DS"),
|
||||
/**
|
||||
* Represents the DNS HTTPS (HTTP Service) record type.
|
||||
* Represents the DNS HTTPS (HTTP Service) getRecord type.
|
||||
*
|
||||
* <p>The "HTTPS" record type is used to specify information about the HTTP/3 and related services
|
||||
* <p>The "HTTPS" getRecord type is used to specify information about the HTTP/3 and related services
|
||||
* offered by a domain.
|
||||
*/
|
||||
HTTPS("HTTPS"),
|
||||
/**
|
||||
* Represents the DNS LOC (Location) record type.
|
||||
* Represents the DNS LOC (Location) getRecord type.
|
||||
*
|
||||
* <p>The "LOC" record type is used to store geographical location information for a domain,
|
||||
* <p>The "LOC" getRecord type is used to store geographical location information for a domain,
|
||||
* including latitude, longitude, altitude, and other details. It enables associating domains with
|
||||
* physical locations.
|
||||
*/
|
||||
LOC("LOC"),
|
||||
/**
|
||||
* Represents the DNS MX (Mail Exchange) record type.
|
||||
* Represents the DNS MX (Mail Exchange) getRecord type.
|
||||
*
|
||||
* <p>The "MX" record type is used to specify the mail servers responsible for receiving email
|
||||
* <p>The "MX" getRecord type is used to specify the mail servers responsible for receiving email
|
||||
* messages on behalf of a domain.
|
||||
*/
|
||||
MX("MX"),
|
||||
/**
|
||||
* Represents the NAPTR record type for DNS configurations.
|
||||
* Represents the NAPTR getRecord type for DNS configurations.
|
||||
*
|
||||
* <p>This constant is used to specify NAPTR (Naming Authority Pointer) DNS records, which allow
|
||||
* for service discovery through flexible DNS-based mechanisms. NAPTR records are commonly used in
|
||||
@@ -99,7 +99,7 @@ public enum RecordType {
|
||||
*/
|
||||
NS("NS"),
|
||||
/**
|
||||
* Represents the "OPENPGPKEY" DNS record type.
|
||||
* Represents the "OPENPGPKEY" DNS getRecord type.
|
||||
*
|
||||
* <p>This constant is primarily used to identify DNS records of the type "OPENPGPKEY". It may be
|
||||
* utilized within the system for operations such as creating, retrieving, updating, or managing
|
||||
@@ -107,17 +107,17 @@ public enum RecordType {
|
||||
*/
|
||||
OPENPGPKEY("OPENPGPKEY"),
|
||||
/**
|
||||
* Represents a DNS record type.
|
||||
* Represents a DNS getRecord type.
|
||||
*
|
||||
* <p>The `PTR` value specifically refers to a "pointer record" in the DNS system, which is
|
||||
* <p>The `PTR` value specifically refers to a "pointer getRecord" in the DNS system, which is
|
||||
* typically used for reverse DNS lookups.
|
||||
*/
|
||||
PTR("PTR"),
|
||||
/**
|
||||
* Represents the SMIMEA DNS record type.
|
||||
* Represents the SMIMEA DNS getRecord type.
|
||||
*
|
||||
* <p>The SMIMEA resource record is used to associate a user's certificate information for email
|
||||
* message signing or encryption. This type of DNS record is part of the DNS-based Authentication
|
||||
* <p>The SMIMEA resource getRecord is used to associate a user's certificate information for email
|
||||
* message signing or encryption. This type of DNS getRecord is part of the DNS-based Authentication
|
||||
* of Named Entities (DANE) protocol.
|
||||
*
|
||||
* <p>SMIMEA records provide a mechanism for utilizing certificates in email communication
|
||||
@@ -134,41 +134,41 @@ public enum RecordType {
|
||||
*/
|
||||
SMIMEA("SMIMEA"),
|
||||
/**
|
||||
* Represents a service record (SRV) type in the DNS configuration model.
|
||||
* Represents a service getRecord (SRV) type in the DNS configuration model.
|
||||
*
|
||||
* <p>This constant may be used to identify and work with SRV record types in various DNS-related
|
||||
* <p>This constant may be used to identify and work with SRV getRecord types in various DNS-related
|
||||
* operations or integrations.
|
||||
*/
|
||||
SRV("SRV"),
|
||||
/**
|
||||
* Represents the DNS record type "SSHFP" (SSH Fingerprint), used in DNS to store cryptographic
|
||||
* Represents the DNS getRecord type "SSHFP" (SSH Fingerprint), used in DNS to store cryptographic
|
||||
* fingerprints associated with SSH host keys.
|
||||
*
|
||||
* <p>This DNS record type provides a mechanism for verifying the authenticity of an SSH server
|
||||
* <p>This DNS getRecord type provides a mechanism for verifying the authenticity of an SSH server
|
||||
* before initiating a connection, enhancing the security of SSH communications.
|
||||
*/
|
||||
SSHFP("SSHFP"),
|
||||
/**
|
||||
* Represents the Service Binding (SVCB) DNS record type.
|
||||
* Represents the Service Binding (SVCB) DNS getRecord type.
|
||||
*
|
||||
* <p>The SVCB record is a DNS resource record used to indicate alternative endpoints or specific
|
||||
* <p>The SVCB getRecord is a DNS resource getRecord used to indicate alternative endpoints or specific
|
||||
* configuration details for services. It is commonly applied in service discovery and
|
||||
* protocol-specific configurations.
|
||||
*/
|
||||
SVCB("SVCB"),
|
||||
/**
|
||||
* Represents a constant for the DNS-based Authentication of Named Entities (DANE) TLSA record
|
||||
* Represents a constant for the DNS-based Authentication of Named Entities (DANE) TLSA getRecord
|
||||
* type.
|
||||
*
|
||||
* <p>The TLSA record is used to associate a TLS server certificate or public key with the domain
|
||||
* <p>The TLSA getRecord is used to associate a TLS server certificate or public key with the domain
|
||||
* name (e.g., via DNSSEC). It enables cryptographically secured connections by attaching
|
||||
* certificate and key constraints to the specific domain.
|
||||
*/
|
||||
TLSA("TLSA"),
|
||||
/**
|
||||
* Represents the TXT DNS record type.
|
||||
* Represents the TXT DNS getRecord type.
|
||||
*
|
||||
* <p>The TXT DNS record type is commonly used to store text-based information for various
|
||||
* <p>The TXT DNS getRecord type is commonly used to store text-based information for various
|
||||
* verification and configuration purposes, such as domain ownership verification or email
|
||||
* authentication protocols (e.g., SPF, DKIM).
|
||||
*/
|
||||
@@ -199,6 +199,6 @@ public enum RecordType {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getType();
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,12 @@ package codes.thischwa.cf.model;
|
||||
* <p>This class contains information about the current page, page size, total pages, and result
|
||||
* counts, which can be utilized in managing and navigating through paginated data.
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>page:</b> The current page number.
|
||||
* <li><b>perPage:</b> The number of results per page.
|
||||
* <li><b>totalPages:</b> The total number of pages available.
|
||||
* <li><b>count:</b> The number of results on the current page.
|
||||
* <li><b>totalCount:</b> The total number of results across all pages.
|
||||
* </ul>
|
||||
* @param page The current page number.
|
||||
* @param perPage The number of results per page.
|
||||
* @param totalPages The total number of pages available.
|
||||
* @param count The number of results on the current page.
|
||||
* @param totalCount The total number of results across all pages.
|
||||
*/
|
||||
|
||||
public record ResultInfo(int page, int perPage, int totalPages, int count, int totalCount) {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,157 @@
|
||||
package codes.thischwa.cf;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
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 codes.thischwa.cf.model.RecordEntity;
|
||||
import codes.thischwa.cf.model.RecordSingleResponse;
|
||||
import codes.thischwa.cf.model.RecordType;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for CfBasicHttpClient using mocked HTTP components.
|
||||
* Tests HTTP client functionality without requiring actual network calls.
|
||||
*/
|
||||
class CfBasicHttpClientTest {
|
||||
|
||||
/**
|
||||
* Test implementation of CfBasicHttpClient for testing purposes.
|
||||
*/
|
||||
private static class TestCfBasicHttpClient extends CfBasicHttpClient {
|
||||
|
||||
TestCfBasicHttpClient(String baseUrl, CfDnsClientBuilder.CfAuth auth) {
|
||||
super(baseUrl, auth);
|
||||
}
|
||||
|
||||
// Expose protected methods for testing
|
||||
public <T extends codes.thischwa.cf.model.AbstractResponse> T testGetRequest(String endpoint, Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
return getRequest(endpoint, responseType);
|
||||
}
|
||||
|
||||
public <T extends codes.thischwa.cf.model.AbstractResponse> T testPostRequest(String endpoint, Object payload, Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
return postRequest(endpoint, payload, responseType);
|
||||
}
|
||||
|
||||
public <T extends codes.thischwa.cf.model.AbstractResponse> T testPutRequest(String endpoint, Object payload, Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
return putRequest(endpoint, payload, responseType);
|
||||
}
|
||||
|
||||
public <T extends codes.thischwa.cf.model.AbstractResponse> T testPatchRequest(String endpoint, Object payload, Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
return patchRequest(endpoint, payload, responseType);
|
||||
}
|
||||
|
||||
public <T extends codes.thischwa.cf.model.AbstractResponse> T testDeleteRequest(String endpoint, Class<T> responseType)
|
||||
throws CloudflareApiException {
|
||||
return deleteRequest(endpoint, responseType);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor_WithApiToken() {
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructor_WithEmailKey() {
|
||||
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth("test@example.com", "test-key");
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testApiException_unknowEndpoint() {
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
|
||||
// Invalid JSON will cause a parsing error
|
||||
CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> {
|
||||
client.testGetRequest("/invalid-endpoint", RecordSingleResponse.class);
|
||||
});
|
||||
|
||||
assertNotNull(exception);
|
||||
assertTrue(exception.getMessage().contains("Unexpected error"));
|
||||
Throwable cause = exception.getCause();
|
||||
assertInstanceOf(CloudflareApiException.class, cause);
|
||||
assertTrue(cause.getMessage().contains("No route for that URI"), "Expected error message: No route for that URI, but it was: " + cause.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResultWrapper() throws Exception {
|
||||
// Test the private ResultWrapper record indirectly through client behavior
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
|
||||
// Any request will create and use a ResultWrapper internally
|
||||
assertThrows(CloudflareApiException.class, () -> {
|
||||
client.testGetRequest("/test", RecordSingleResponse.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuthApplicationHeader_ApiToken() {
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("my-token");
|
||||
HttpGet request = new HttpGet("https://api.cloudflare.com/test");
|
||||
|
||||
auth.applyAuth(request);
|
||||
|
||||
assertTrue(request.containsHeader("Authorization"));
|
||||
assertEquals("Bearer my-token", request.getFirstHeader("Authorization").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuthApplicationHeader_EmailKey() {
|
||||
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth("test@example.com", "my-key");
|
||||
HttpGet request = new HttpGet("https://api.cloudflare.com/test");
|
||||
|
||||
auth.applyAuth(request);
|
||||
|
||||
assertTrue(request.containsHeader("X-Auth-Email"));
|
||||
assertTrue(request.containsHeader("X-Auth-Key"));
|
||||
assertEquals("test@example.com", request.getFirstHeader("X-Auth-Email").getValue());
|
||||
assertEquals("my-key", request.getFirstHeader("X-Auth-Key").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBaseUrlConstruction() {
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
|
||||
|
||||
// Test default base URL
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testObjectMapperInitialization() {
|
||||
// ObjectMapper is initialized in constructor via JsonConf
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
|
||||
// If ObjectMapper wasn't initialized, any request would fail with NullPointerException
|
||||
assertThrows(CloudflareApiException.class, () -> {
|
||||
client.testGetRequest("/test", RecordSingleResponse.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRequestPayloadSerialization() {
|
||||
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
|
||||
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
|
||||
|
||||
RecordEntity record = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
|
||||
// The payload serialization happens inside postRequest
|
||||
assertThrows(CloudflareApiException.class, () -> {
|
||||
client.testPostRequest("/test", record, RecordSingleResponse.class);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -22,32 +22,30 @@ public class CfClientPenTest {
|
||||
|
||||
private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone
|
||||
|
||||
private static final String API_EMAIL = System.getenv("API_EMAIL");
|
||||
private static final String API_KEY = System.getenv("API_KEY");
|
||||
private static final String API_TOKEN = System.getenv("API_TOKEN");
|
||||
|
||||
@BeforeAll
|
||||
static void checkEnv() {
|
||||
assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests");
|
||||
assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests");
|
||||
assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping pen tests");
|
||||
}
|
||||
|
||||
private CfDnsClient newClient() {
|
||||
return new CfDnsClient(API_EMAIL, API_KEY);
|
||||
return new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException")
|
||||
void testInvalidCredentialsShouldFail() {
|
||||
// Use syntactically valid but wrong credentials
|
||||
CfDnsClient badClient = new CfDnsClient("invalid@example.com", UUID.randomUUID().toString());
|
||||
assertThrows(CloudflareApiException.class, badClient::zoneListAll);
|
||||
CfDnsClient badClient = new CfDnsClientBuilder().withEmailKeyAuth("invalid@example.com", UUID.randomUUID().toString()).build();
|
||||
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,20 +56,20 @@ 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 + "'");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Invalid record content and TTL boundaries must be rejected by API")
|
||||
@DisplayName("Invalid getRecord 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;
|
||||
@@ -83,11 +81,11 @@ public class CfClientPenTest {
|
||||
}
|
||||
|
||||
try {
|
||||
// A record with invalid IPv4
|
||||
// A getRecord with invalid IPv4
|
||||
assertThrows(CloudflareApiException.class,
|
||||
() -> client.recordCreate(zone, fqdn, 60, RecordType.A, "999.999.999.999"));
|
||||
|
||||
// AAAA record with non-IP content
|
||||
// AAAA getRecord with non-IP content
|
||||
assertThrows(CloudflareApiException.class,
|
||||
() -> client.recordCreate(zone, fqdn, 60, RecordType.AAAA, "not-an-ipv6"));
|
||||
|
||||
@@ -106,7 +104,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(
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
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.PagingRequest;
|
||||
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.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -22,45 +32,102 @@ public class CfClientTest {
|
||||
private static final String SLD_STR = "devsld";
|
||||
private static final int TTL = 60;
|
||||
|
||||
private static final String API_EMAIL = System.getenv("API_EMAIL");
|
||||
private static final String API_KEY = System.getenv("API_KEY");
|
||||
private static final String API_TOKEN = System.getenv("API_TOKEN");
|
||||
|
||||
private final CfDnsClient client = new CfDnsClient(API_EMAIL, API_KEY);
|
||||
private final CfDnsClient client = new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
|
||||
|
||||
@BeforeAll
|
||||
static void checkEnv() {
|
||||
assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests");
|
||||
assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests");
|
||||
assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping client tests");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnknownSld() throws Exception {
|
||||
ZoneEntity zone = client.zoneGet(ZONE_STR);
|
||||
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, "unknown", RecordType.A));
|
||||
}
|
||||
|
||||
@Test
|
||||
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.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 testZoneList() throws CloudflareApiException {
|
||||
List<ZoneEntity> zones = client.zoneList();
|
||||
assertNotNull(zones);
|
||||
assertFalse(zones.isEmpty());
|
||||
assertEquals(ZONE_STR, zones.get(0).getName());
|
||||
|
||||
zones = client.zoneList(PagingRequest.of(1, 100));
|
||||
assertNotNull(zones);
|
||||
assertFalse(zones.isEmpty());
|
||||
assertEquals(ZONE_STR, zones.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneListAnlFailedSldList() throws Exception {
|
||||
List<ZoneEntity> zList = client.zoneListAll();
|
||||
List<ZoneEntity> zList = client.zoneList();
|
||||
assertEquals(1, zList.size());
|
||||
|
||||
assertThrows(CloudflareNotFoundException.class,
|
||||
() -> client.sldListAll(zList.get(0), "not-existing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyResultThrowsException() throws Exception {
|
||||
List<ZoneEntity> zList = client.zoneListAll();
|
||||
CfDnsClient client = new CfDnsClient(true, API_EMAIL, API_KEY);
|
||||
assertThrows(CloudflareNotFoundException.class,
|
||||
() -> client.sldListAll(zList.get(0), "not-existing"));
|
||||
() -> 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);
|
||||
assertEquals("0a83dd6e7f8c46039f2517bbded8115e", z.getId());
|
||||
ZoneEntity z = client.zoneGet(ZONE_STR);
|
||||
assertEquals("cf9d8b12f61423f280e0a3ea2a96d921", z.getId());
|
||||
assertEquals("mein-d-ns.de", z.getName());
|
||||
assertEquals("active", z.getStatus());
|
||||
assertEquals(2, z.getNameServers().size());
|
||||
assertTrue(z.getNameServers().contains("sergi.ns.cloudflare.com"));
|
||||
assertEquals(4, z.getOriginalNameServers().size());
|
||||
assertTrue(z.getOriginalNameServers().contains("a.ns14.net"));
|
||||
assertTrue(z.getNameServers().contains("rafe.ns.cloudflare.com"));
|
||||
assertTrue(z.getOriginalNameServers().size() >= 2);
|
||||
assertTrue(z.getOriginalNameServers().contains("blair.ns.cloudflare.com"));
|
||||
assertNotNull(z.getActivatedOn());
|
||||
assertNotNull(z.getModifiedOn());
|
||||
assertNotNull(z.getCreatedOn());
|
||||
@@ -77,76 +144,385 @@ public class CfClientTest {
|
||||
// ensure clean state
|
||||
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A, RecordType.AAAA);
|
||||
|
||||
// create A record using recordCreate with full domain
|
||||
// create A getRecord using recordCreate with full domain
|
||||
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
|
||||
// create AAAA getRecord 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 getRecord type: %s", re.getType()));
|
||||
}
|
||||
}
|
||||
|
||||
// update AAAA record
|
||||
// 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.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 getRecord
|
||||
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);
|
||||
// verify A getRecord still intact
|
||||
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
|
||||
// delete AAAA getRecord 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
|
||||
// delete A getRecord 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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient(null, "key"));
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", null));
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", ""));
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("", "key"));
|
||||
void testRecordEntityInvalidType() {
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> RecordEntity.build("id123", "example.com", "INVALID_TYPE", 60, "192.168.1.1"));
|
||||
assertTrue(exception.getMessage().contains("Invalid getRecord 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)
|
||||
.getRecord(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)
|
||||
.getRecord(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)
|
||||
.getRecord(fluentSld, RecordType.A)
|
||||
.update("192.168.100.2");
|
||||
|
||||
assertEquals("192.168.100.2", updated.getContent());
|
||||
|
||||
// Test fluent delete
|
||||
client.zone(ZONE_STR)
|
||||
.getRecord(fluentSld)
|
||||
.delete(RecordType.A);
|
||||
|
||||
assertThrows(CloudflareNotFoundException.class,
|
||||
() -> client.zone(ZONE_STR).getRecord(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 getRecord.");
|
||||
}
|
||||
|
||||
@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.");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testPaging() throws Exception {
|
||||
ZoneEntity zone = client.zoneGet(ZONE_STR);
|
||||
String pagingSld = "paging-" + System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
int existingCount = 0;
|
||||
try {
|
||||
List<RecordEntity> allRecords = client.recordList(zone);
|
||||
existingCount = allRecords.size();
|
||||
} catch (CloudflareApiException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Calculate how many records we need to create to reach at least 12 total A records
|
||||
// (to test paging with pageSize 5: page 1 = 5, page 2 = 5, page 3 = 2+)
|
||||
int targetCount = 12;
|
||||
int recordsToCreate = Math.max(0, targetCount - existingCount);
|
||||
|
||||
// Create additional A records if needed
|
||||
List<RecordEntity> createdRecords = new ArrayList<>();
|
||||
for (int i = 1; i <= recordsToCreate; i++) {
|
||||
RecordEntity record = RecordEntity.build(pagingSld, RecordType.A, TTL, "127.0.0." + i);
|
||||
RecordEntity created = client.recordCreate(zone, record);
|
||||
createdRecords.add(created);
|
||||
assertNotNull(created.getId());
|
||||
}
|
||||
|
||||
// Test paging with page size of 5
|
||||
PagingRequest page1Request = PagingRequest.of(1, 5);
|
||||
List<RecordEntity> page1Records = client.recordList(zone, page1Request);
|
||||
assertEquals(5, page1Records.size(), "First page should contain 5 records");
|
||||
|
||||
// 2nd page should also contain 5 records (if we have at least 12 total)
|
||||
PagingRequest page2Request = PagingRequest.of(2, 5);
|
||||
List<RecordEntity> page2Records = client.recordList(zone, page2Request);
|
||||
assertEquals(5, page2Records.size(), "Second page should contain at least 5 records");
|
||||
|
||||
// 3rd page should contain 2 records
|
||||
PagingRequest page3Request = PagingRequest.of(3, 5);
|
||||
List<RecordEntity> page3Records = client.recordList(zone, page3Request);
|
||||
assertEquals(2, page3Records.size(), "Third page should contain 2 records");
|
||||
|
||||
// Verify no overlap between pages
|
||||
List<String> page1Ids = page1Records.stream().map(RecordEntity::getId).toList();
|
||||
List<String> page2Ids = page2Records.stream().map(RecordEntity::getId).toList();
|
||||
List<String> page3Ids = page3Records.stream().map(RecordEntity::getId).toList();
|
||||
Set<String> generatedRecordIds = new HashSet<>(page1Ids);
|
||||
generatedRecordIds.addAll(page2Ids);
|
||||
generatedRecordIds.addAll(page3Ids);
|
||||
assertEquals(createdRecords.size(), generatedRecordIds.size());
|
||||
|
||||
// Verify our created records are in the zone
|
||||
List<RecordEntity> allRecords = client.recordList(zone);
|
||||
Set<String> allRecordIds = allRecords.stream().map(RecordEntity::getId).collect(Collectors.toSet());
|
||||
assertEquals(createdRecords.size(), allRecordIds.size());
|
||||
assertTrue(allRecordIds.containsAll(generatedRecordIds));
|
||||
} finally {
|
||||
try {
|
||||
client.recordDeleteTypeIfExists(zone, pagingSld, RecordType.A);
|
||||
} catch (Exception e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package codes.thischwa.cf;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for CfDnsClientBuilder and its authentication classes.
|
||||
*/
|
||||
class CfDnsClientBuilderTest {
|
||||
|
||||
@Test
|
||||
void testBuildWithApiToken() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithEmailKey() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withEmailKeyAuth("test@example.com", "test-key")
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithCustomBaseUrl() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.withBaseUrl("https://custom-api.example.com")
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithDefaultBaseUrl() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithEmptyResultThrowsException() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.withEmptyResultThrowsException(true)
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithEmptyResultDoesNotThrowException() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.withEmptyResultThrowsException(false)
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderMethodChaining() {
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.withBaseUrl("https://custom-api.example.com")
|
||||
.withEmptyResultThrowsException(true)
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testApiTokenAuth_BlankToken() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.ApiTokenAuth(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailKeyAuth_ValidCredentials() {
|
||||
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth(
|
||||
"test@example.com",
|
||||
"valid-key"
|
||||
);
|
||||
assertNotNull(auth);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailKeyAuth_BlankEmail() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.EmailKeyAuth(" ", "valid-key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailKeyAuth_BlankKey() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.EmailKeyAuth("test@example.com", " "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailKeyAuth_BothBlank() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.EmailKeyAuth(" ", " "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultBaseUrl() {
|
||||
assertEquals("https://api.cloudflare.com/client/v4", CfDnsClientBuilder.DEFAULT_BASEURL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderWithMultipleConfigurations() {
|
||||
// Test switching auth methods in the same builder (last one wins)
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withEmailKeyAuth("test@example.com", "old-key")
|
||||
.withApiTokenAuth("new-token") // This should override the email/key auth
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderWithMultipleBaseUrls() {
|
||||
// Test setting base URL multiple times (last one wins)
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.withBaseUrl("https://old-api.example.com")
|
||||
.withBaseUrl("https://new-api.example.com") // This should override
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyResultThrowsExceptionToggle() {
|
||||
// Test toggling the flag multiple times (last one wins)
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.withEmptyResultThrowsException(true)
|
||||
.withEmptyResultThrowsException(false) // This should override
|
||||
.build();
|
||||
|
||||
assertNotNull(client);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package codes.thischwa.cf;
|
||||
|
||||
import codes.thischwa.cf.fluent.ZoneOperations;
|
||||
import codes.thischwa.cf.model.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for CfDnsClient using mocked HTTP client responses.
|
||||
* Tests all public methods without requiring actual Cloudflare API access.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CfDnsClientMockTest {
|
||||
|
||||
private CfDnsClient client;
|
||||
|
||||
@Mock
|
||||
private CfBasicHttpClient mockHttpClient;
|
||||
|
||||
private static final String TEST_ZONE_ID = "zone123";
|
||||
private static final String TEST_ZONE_NAME = "example.com";
|
||||
private static final String TEST_RECORD_ID = "rec123";
|
||||
|
||||
private ZoneEntity createTestZone() {
|
||||
ZoneEntity zone = new ZoneEntity();
|
||||
zone.setId(TEST_ZONE_ID);
|
||||
zone.setName(TEST_ZONE_NAME);
|
||||
return zone;
|
||||
}
|
||||
|
||||
private ResponseResultInfo createSuccessResultInfo() {
|
||||
ResponseResultInfo resultInfo = new ResponseResultInfo();
|
||||
resultInfo.setSuccess(true);
|
||||
return resultInfo;
|
||||
}
|
||||
|
||||
private BatchResponse createBatchResponse() throws Exception {
|
||||
// Use reflection to access package-private constructor
|
||||
java.lang.reflect.Constructor<BatchResponse> constructor = BatchResponse.class.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance();
|
||||
}
|
||||
|
||||
private ResultInfo createResultInfo(int count) {
|
||||
return new ResultInfo(count);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
// Create a real client with API token auth
|
||||
client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("test-token")
|
||||
.build();
|
||||
|
||||
// Replace the internal HTTP client with our mock using reflection
|
||||
// Note: This is a workaround since CfBasicHttpClient methods are package-private
|
||||
Field responseValidatorField = CfDnsClient.class.getDeclaredField("responseValidator");
|
||||
responseValidatorField.setAccessible(true);
|
||||
responseValidatorField.set(client, new ResponseValidator(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGroupRecordsByFqdn() {
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity rec2 = RecordEntity.build("test.example.com", RecordType.AAAA, 300, "::1");
|
||||
RecordEntity rec3 = RecordEntity.build("www.example.com", RecordType.A, 300, "1.2.3.5");
|
||||
|
||||
List<RecordEntity> records = List.of(rec1, rec2, rec3);
|
||||
Map<String, List<RecordEntity>> grouped = CfDnsClient.groupRecordsByFqdn(records);
|
||||
|
||||
assertEquals(2, grouped.size());
|
||||
assertEquals(2, grouped.get("test.example.com").size());
|
||||
assertEquals(1, grouped.get("www.example.com").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGroupRecordsByFqdn_NullInput() {
|
||||
Map<String, List<RecordEntity>> grouped = CfDnsClient.groupRecordsByFqdn(null);
|
||||
assertNotNull(grouped);
|
||||
assertTrue(grouped.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneList() throws Exception {
|
||||
// Create mock zone response
|
||||
ZoneMultipleResponse mockResponse = new ZoneMultipleResponse();
|
||||
ZoneEntity zone1 = new ZoneEntity();
|
||||
zone1.setId("zone1");
|
||||
zone1.setName("example.com");
|
||||
ZoneEntity zone2 = new ZoneEntity();
|
||||
zone2.setId("zone2");
|
||||
zone2.setName("test.com");
|
||||
mockResponse.setResult(List.of(zone1, zone2));
|
||||
ResponseResultInfo resultInfo = new ResponseResultInfo();
|
||||
resultInfo.setSuccess(true);
|
||||
mockResponse.setResponseResultInfo(resultInfo);
|
||||
|
||||
// Mock the HTTP client via spy
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(ZoneMultipleResponse.class));
|
||||
|
||||
List<ZoneEntity> zones = spyClient.zoneList();
|
||||
|
||||
assertNotNull(zones);
|
||||
assertEquals(2, zones.size());
|
||||
assertEquals("example.com", zones.get(0).getName());
|
||||
assertEquals("test.com", zones.get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneGet() throws Exception {
|
||||
// Create mock zone response
|
||||
ZoneMultipleResponse mockResponse = new ZoneMultipleResponse();
|
||||
ZoneEntity zone = new ZoneEntity();
|
||||
zone.setId(TEST_ZONE_ID);
|
||||
zone.setName(TEST_ZONE_NAME);
|
||||
mockResponse.setResult(List.of(zone));
|
||||
ResponseResultInfo resultInfo = new ResponseResultInfo();
|
||||
resultInfo.setSuccess(true);
|
||||
mockResponse.setResponseResultInfo(resultInfo);
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(ZoneMultipleResponse.class));
|
||||
|
||||
ZoneEntity result = spyClient.zoneGet(TEST_ZONE_NAME);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(TEST_ZONE_ID, result.getId());
|
||||
assertEquals(TEST_ZONE_NAME, result.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneOperations() throws Exception {
|
||||
// Create mock zone response
|
||||
ZoneMultipleResponse mockResponse = new ZoneMultipleResponse();
|
||||
ZoneEntity zone = new ZoneEntity();
|
||||
zone.setId(TEST_ZONE_ID);
|
||||
zone.setName(TEST_ZONE_NAME);
|
||||
mockResponse.setResult(List.of(zone));
|
||||
ResponseResultInfo resultInfo = new ResponseResultInfo();
|
||||
resultInfo.setSuccess(true);
|
||||
mockResponse.setResponseResultInfo(resultInfo);
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(ZoneMultipleResponse.class));
|
||||
|
||||
ZoneOperations ops = spyClient.zone(TEST_ZONE_NAME);
|
||||
|
||||
assertNotNull(ops);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordList_Zone() throws Exception {
|
||||
ZoneEntity zone = new ZoneEntity();
|
||||
zone.setId(TEST_ZONE_ID);
|
||||
zone.setName(TEST_ZONE_NAME);
|
||||
|
||||
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
|
||||
mockResponse.setResultInfo(createResultInfo(1));
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity rec2 = RecordEntity.build("www.example.com", RecordType.A, 300, "1.2.3.5");
|
||||
mockResponse.setResult(List.of(rec1, rec2));
|
||||
ResponseResultInfo resultInfo = new ResponseResultInfo();
|
||||
resultInfo.setSuccess(true);
|
||||
mockResponse.setResponseResultInfo(resultInfo);
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
|
||||
List<RecordEntity> records = spyClient.recordList(zone);
|
||||
|
||||
assertNotNull(records);
|
||||
assertEquals(2, records.size());
|
||||
assertEquals("1.2.3.4", records.get(0).getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordList_WithPaging() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
PagingRequest pagingRequest = PagingRequest.of(10, 1);
|
||||
|
||||
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
|
||||
mockResponse.setResultInfo(createResultInfo(1));
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
mockResponse.setResult(List.of(rec1));
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
|
||||
List<RecordEntity> records = spyClient.recordList(zone, pagingRequest);
|
||||
|
||||
assertNotNull(records);
|
||||
assertEquals(1, records.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordList_BySld() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
|
||||
mockResponse.setResultInfo(createResultInfo(1));
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
mockResponse.setResult(List.of(rec1));
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
|
||||
List<RecordEntity> records = spyClient.recordList(zone, "test");
|
||||
|
||||
assertNotNull(records);
|
||||
assertEquals(1, records.size());
|
||||
assertEquals(TEST_ZONE_ID, records.get(0).getZoneId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordList_BySldAndType() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
|
||||
mockResponse.setResultInfo(createResultInfo(1));
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity rec2 = RecordEntity.build("test.example.com", RecordType.AAAA, 300, "::1");
|
||||
mockResponse.setResult(List.of(rec1, rec2));
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
|
||||
List<RecordEntity> records = spyClient.recordList(zone, "test", RecordType.A);
|
||||
|
||||
assertNotNull(records);
|
||||
assertEquals(1, records.size());
|
||||
assertEquals(RecordType.A, RecordType.valueOf(records.get(0).getType()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordList_ByType() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
|
||||
mockResponse.setResultInfo(createResultInfo(1));
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity rec2 = RecordEntity.build("www.example.com", RecordType.AAAA, 300, "::1");
|
||||
mockResponse.setResult(List.of(rec1, rec2));
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
|
||||
List<RecordEntity> records = spyClient.recordList(zone, RecordType.A);
|
||||
|
||||
assertNotNull(records);
|
||||
assertEquals(1, records.size());
|
||||
assertEquals(RecordType.A, RecordType.valueOf(records.get(0).getType()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordCreateSld() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordSingleResponse mockResponse = new RecordSingleResponse();
|
||||
RecordEntity createdRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
createdRecord.setId(TEST_RECORD_ID);
|
||||
mockResponse.setResult(createdRecord);
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).postRequest(anyString(), any(), eq(RecordSingleResponse.class));
|
||||
|
||||
RecordEntity result = spyClient.recordCreate(zone, "test.example.com", 300, RecordType.A, "1.2.3.4");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(TEST_RECORD_ID, result.getId());
|
||||
assertEquals(TEST_ZONE_ID, result.getZoneId());
|
||||
assertEquals("1.2.3.4", result.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordDelete() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordSingleResponse mockResponse = new RecordSingleResponse();
|
||||
RecordEntity deletedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
deletedRecord.setId(TEST_RECORD_ID);
|
||||
mockResponse.setResult(deletedRecord);
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).deleteRequest(anyString(), eq(RecordSingleResponse.class));
|
||||
|
||||
boolean result = spyClient.recordDelete(zone, TEST_RECORD_ID);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordUpdate() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
RecordEntity record = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
record.setId(TEST_RECORD_ID);
|
||||
|
||||
RecordSingleResponse mockResponse = new RecordSingleResponse();
|
||||
RecordEntity updatedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.5");
|
||||
updatedRecord.setId(TEST_RECORD_ID);
|
||||
mockResponse.setResult(updatedRecord);
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).patchRequest(anyString(), any(), eq(RecordSingleResponse.class));
|
||||
|
||||
RecordEntity result = spyClient.recordUpdate(zone, record);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(TEST_RECORD_ID, result.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordDeleteTypeIfExists() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordMultipleResponse listResponse = new RecordMultipleResponse();
|
||||
listResponse.setResultInfo(createResultInfo(1));
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
rec1.setId(TEST_RECORD_ID);
|
||||
listResponse.setResult(List.of(rec1));
|
||||
listResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
RecordSingleResponse deleteResponse = new RecordSingleResponse();
|
||||
RecordEntity deletedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
deletedRecord.setId(TEST_RECORD_ID);
|
||||
deleteResponse.setResult(deletedRecord);
|
||||
deleteResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(listResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
doReturn(deleteResponse).when(spyClient).deleteRequest(anyString(), eq(RecordSingleResponse.class));
|
||||
|
||||
// Should not throw exception
|
||||
spyClient.recordDeleteTypeIfExists(zone, "test", RecordType.A);
|
||||
|
||||
verify(spyClient, times(1)).getRequest(anyString(), eq(RecordMultipleResponse.class));
|
||||
verify(spyClient, times(1)).deleteRequest(anyString(), eq(RecordSingleResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordDeleteTypeIfExists_NotFound() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doThrow(new CloudflareNotFoundException("Not found")).when(spyClient).recordList(any(), anyString(), any());
|
||||
|
||||
// Should not throw exception when record doesn't exist
|
||||
assertDoesNotThrow(() -> spyClient.recordDeleteTypeIfExists(zone, "test", RecordType.A));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordBatch() throws Exception {
|
||||
ZoneEntity zone = createTestZone();
|
||||
|
||||
RecordEntity postRecord = RecordEntity.build("new.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity patchRecord = RecordEntity.build(TEST_RECORD_ID, "1.2.3.5");
|
||||
RecordEntity deleteRecord = RecordEntity.build("old.example.com", RecordType.A, 300, "1.2.3.6");
|
||||
deleteRecord.setId("rec999");
|
||||
|
||||
BatchResponse mockResponse = createBatchResponse();
|
||||
BatchEntry resultEntry = new BatchEntry();
|
||||
resultEntry.setPosts(List.of(postRecord));
|
||||
resultEntry.setPatches(List.of(patchRecord));
|
||||
mockResponse.setResult(resultEntry);
|
||||
mockResponse.setResponseResultInfo(createSuccessResultInfo());
|
||||
|
||||
CfDnsClient spyClient = spy(client);
|
||||
doReturn(mockResponse).when(spyClient).postRequest(anyString(), any(), eq(BatchResponse.class));
|
||||
|
||||
BatchEntry result = spyClient.recordBatch(zone,
|
||||
List.of(postRecord),
|
||||
null,
|
||||
List.of(patchRecord),
|
||||
List.of(deleteRecord));
|
||||
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getPosts());
|
||||
assertEquals(1, result.getPosts().size());
|
||||
assertEquals(TEST_ZONE_ID, result.getPosts().get(0).getZoneId());
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -28,7 +27,7 @@ public class CfRequestTest {
|
||||
|
||||
@Test
|
||||
public void testBuildRecordInfoName() {
|
||||
String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sub.domain.com");
|
||||
String result = CfRequest.RECORD_LIST_NAME.buildPath("zone123", "sub.domain.com");
|
||||
assertEquals("/zones/zone123/dns_records?name=sub.domain.com", result);
|
||||
}
|
||||
|
||||
@@ -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_LIST_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);
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package codes.thischwa.cf.fluent;
|
||||
|
||||
import codes.thischwa.cf.CfDnsClient;
|
||||
import codes.thischwa.cf.CloudflareApiException;
|
||||
import codes.thischwa.cf.model.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for the Fluent API package (ZoneOperations and RecordOperations).
|
||||
*/
|
||||
class FluentApiTest {
|
||||
|
||||
private CfDnsClient mockClient;
|
||||
private ZoneEntity testZone;
|
||||
|
||||
private static final String TEST_ZONE_ID = "zone123";
|
||||
private static final String TEST_ZONE_NAME = "example.com";
|
||||
private static final String TEST_SLD = "test";
|
||||
private static final String TEST_RECORD_ID = "rec123";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockClient = mock(CfDnsClient.class);
|
||||
testZone = new ZoneEntity();
|
||||
testZone.setId(TEST_ZONE_ID);
|
||||
testZone.setName(TEST_ZONE_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneOperations_GetRecord() throws CloudflareApiException {
|
||||
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
|
||||
|
||||
RecordOperations recordOps = zoneOps.getRecord(TEST_SLD);
|
||||
|
||||
assertNotNull(recordOps);
|
||||
assertInstanceOf(RecordOperationsImpl.class, recordOps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneOperations_GetRecordWithTypes() throws CloudflareApiException {
|
||||
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
|
||||
|
||||
RecordOperations recordOps = zoneOps.getRecord(TEST_SLD, RecordType.A, RecordType.AAAA);
|
||||
|
||||
assertNotNull(recordOps);
|
||||
assertInstanceOf(RecordOperationsImpl.class, recordOps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneOperations_List() throws CloudflareApiException {
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity rec2 = RecordEntity.build("www.example.com", RecordType.A, 300, "1.2.3.5");
|
||||
List<RecordEntity> expectedRecords = List.of(rec1, rec2);
|
||||
|
||||
when(mockClient.recordList(eq(testZone), any(RecordType[].class)))
|
||||
.thenReturn(expectedRecords);
|
||||
|
||||
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
|
||||
List<RecordEntity> result = zoneOps.list();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), any(RecordType[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneOperations_ListWithTypes() throws CloudflareApiException {
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
List<RecordEntity> expectedRecords = List.of(rec1);
|
||||
|
||||
when(mockClient.recordList(eq(testZone), any(RecordType[].class)))
|
||||
.thenReturn(expectedRecords);
|
||||
|
||||
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
|
||||
List<RecordEntity> result = zoneOps.list(RecordType.A);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), any(RecordType[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_Get() throws CloudflareApiException {
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
List<RecordEntity> expectedRecords = List.of(rec1);
|
||||
|
||||
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
|
||||
.thenReturn(expectedRecords);
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
List<RecordEntity> result = recordOps.get();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("1.2.3.4", result.get(0).getContent());
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_GetWithTypes() throws CloudflareApiException {
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
List<RecordEntity> expectedRecords = List.of(rec1);
|
||||
|
||||
RecordType[] types = {RecordType.A};
|
||||
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), eq(types)))
|
||||
.thenReturn(expectedRecords);
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, types);
|
||||
List<RecordEntity> result = recordOps.get();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), eq(types));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_Create() throws CloudflareApiException {
|
||||
RecordEntity createdRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
createdRecord.setId(TEST_RECORD_ID);
|
||||
|
||||
when(mockClient.recordCreateSld(eq(testZone), eq(TEST_SLD), eq(300), eq(RecordType.A), eq("1.2.3.4")))
|
||||
.thenReturn(createdRecord);
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
RecordEntity result = recordOps.create(RecordType.A, "1.2.3.4", 300);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(TEST_RECORD_ID, result.getId());
|
||||
assertEquals("1.2.3.4", result.getContent());
|
||||
verify(mockClient, times(1)).recordCreateSld(eq(testZone), eq(TEST_SLD), eq(300), eq(RecordType.A), eq("1.2.3.4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_Update_Success() throws CloudflareApiException {
|
||||
RecordEntity existingRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
existingRecord.setId(TEST_RECORD_ID);
|
||||
|
||||
RecordEntity updatedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.5");
|
||||
updatedRecord.setId(TEST_RECORD_ID);
|
||||
|
||||
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
|
||||
.thenReturn(List.of(existingRecord));
|
||||
when(mockClient.recordUpdate(eq(testZone), any(RecordEntity.class)))
|
||||
.thenReturn(updatedRecord);
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
RecordEntity result = recordOps.update("1.2.3.5");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("1.2.3.5", result.getContent());
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
|
||||
verify(mockClient, times(1)).recordUpdate(eq(testZone), any(RecordEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_Update_NoRecordsFound() throws CloudflareApiException {
|
||||
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
|
||||
CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> {
|
||||
recordOps.update("1.2.3.5");
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("No recs found"));
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
|
||||
verify(mockClient, never()).recordUpdate(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_Update_MultipleRecordsFound() throws CloudflareApiException {
|
||||
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
RecordEntity rec2 = RecordEntity.build("test.example.com", RecordType.AAAA, 300, "::1");
|
||||
|
||||
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
|
||||
.thenReturn(List.of(rec1, rec2));
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
|
||||
CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> {
|
||||
recordOps.update("1.2.3.5");
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("Multiple recs found"));
|
||||
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
|
||||
verify(mockClient, never()).recordUpdate(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_Delete() throws CloudflareApiException {
|
||||
doNothing().when(mockClient).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
recordOps.delete(RecordType.A, RecordType.AAAA);
|
||||
|
||||
verify(mockClient, times(1)).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordOperations_DeleteSingleType() throws CloudflareApiException {
|
||||
doNothing().when(mockClient).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
|
||||
|
||||
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
|
||||
recordOps.delete(RecordType.A);
|
||||
|
||||
verify(mockClient, times(1)).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFluentApiChaining() throws CloudflareApiException {
|
||||
// Test that fluent API chaining works correctly
|
||||
RecordEntity createdRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
|
||||
createdRecord.setId(TEST_RECORD_ID);
|
||||
|
||||
when(mockClient.recordCreateSld(eq(testZone), eq(TEST_SLD), eq(300), eq(RecordType.A), eq("1.2.3.4")))
|
||||
.thenReturn(createdRecord);
|
||||
|
||||
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
|
||||
RecordEntity result = zoneOps.getRecord(TEST_SLD).create(RecordType.A, "1.2.3.4", 300);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(TEST_RECORD_ID, result.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorFields() {
|
||||
// Test that constructor properly initializes fields
|
||||
RecordType[] types = {RecordType.A, RecordType.AAAA};
|
||||
RecordOperationsImpl recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, types);
|
||||
|
||||
assertNotNull(recordOps);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZoneOperationsImplConstructor() {
|
||||
ZoneOperationsImpl zoneOps = new ZoneOperationsImpl(mockClient, testZone);
|
||||
|
||||
assertNotNull(zoneOps);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package codes.thischwa.cf.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class BatchEntryTest {
|
||||
|
||||
@Test
|
||||
void testBatchEntry() {
|
||||
BatchEntry entry = new BatchEntry();
|
||||
assertEquals("", entry.getId());
|
||||
|
||||
List<RecordEntity> patches = new ArrayList<>();
|
||||
List<RecordEntity> posts = new ArrayList<>();
|
||||
List<RecordEntity> puts = new ArrayList<>();
|
||||
List<RecordEntity> deletes = new ArrayList<>();
|
||||
|
||||
entry.setPatches(patches);
|
||||
entry.setPosts(posts);
|
||||
entry.setPuts(puts);
|
||||
entry.setDeletes(deletes);
|
||||
|
||||
assertSame(patches, entry.getPatches());
|
||||
assertSame(posts, entry.getPosts());
|
||||
assertSame(puts, entry.getPuts());
|
||||
assertSame(deletes, entry.getDeletes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLombokMethods() {
|
||||
BatchEntry entry1 = new BatchEntry();
|
||||
BatchEntry entry2 = new BatchEntry();
|
||||
assertEquals(entry1, entry2);
|
||||
assertEquals(entry1.hashCode(), entry2.hashCode());
|
||||
|
||||
List<RecordEntity> patches = List.of(new RecordEntity());
|
||||
entry1.setPatches(patches);
|
||||
assertNotEquals(entry1, entry2);
|
||||
|
||||
entry2.setPatches(patches);
|
||||
assertEquals(entry1, entry2);
|
||||
|
||||
assertTrue(entry1.toString().contains("patches="));
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,37 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
public class PagingRequestTest {
|
||||
|
||||
@Test
|
||||
public void testBuildPath() {
|
||||
void testBuildPath() {
|
||||
String result = PagingRequest.defaultPaging().addQueryString("/zones");
|
||||
assertEquals("/zones?page=1&perPage=5000000", result);
|
||||
assertEquals("/zones?page=1&per_page=1000", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildPathAdditional() {
|
||||
void testBuildPathAdditional() {
|
||||
String result = new PagingRequest( 10, 100).addQueryString("/zones?foo=bar");
|
||||
assertEquals("/zones?foo=bar&page=10&perPage=100", result);
|
||||
assertEquals("/zones?foo=bar&page=10&per_page=100", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetPagingParams() {
|
||||
PagingRequest request = PagingRequest.of(2, 50);
|
||||
java.util.Map<String, String> params = request.getPagingParams();
|
||||
assertEquals("2", params.get("page"));
|
||||
assertEquals("50", params.get("perPage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLombokMethods() {
|
||||
PagingRequest req1 = PagingRequest.of(1, 10);
|
||||
PagingRequest req2 = PagingRequest.of(1, 10);
|
||||
assertEquals(req1, req2);
|
||||
assertEquals(req1.hashCode(), req2.hashCode());
|
||||
assertTrue(req1.toString().contains("page=1"));
|
||||
|
||||
req1.setPage(5);
|
||||
assertEquals(5, req1.getPage());
|
||||
req1.setPerPage(20);
|
||||
assertEquals(20, req1.getPerPage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package codes.thischwa.cf.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class RecordEntityTest {
|
||||
|
||||
@Test
|
||||
void testBuildWithAttributes() {
|
||||
RecordEntity rec = RecordEntity.build("example.com", RecordType.A, 120, "1.2.3.4");
|
||||
assertEquals("example.com", rec.getName());
|
||||
assertEquals("A", rec.getType());
|
||||
assertEquals(120, rec.getTtl());
|
||||
assertEquals("1.2.3.4", rec.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithIdAndContent() {
|
||||
RecordEntity rec = RecordEntity.build("id-123", "1.2.3.4");
|
||||
assertEquals("id-123", rec.getId());
|
||||
assertEquals("1.2.3.4", rec.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithIdAndAttributes() {
|
||||
RecordEntity rec = RecordEntity.build("id-123", "example.com", "A", 120, "1.2.3.4");
|
||||
assertEquals("id-123", rec.getId());
|
||||
assertEquals("example.com", rec.getName());
|
||||
assertEquals("A", rec.getType());
|
||||
assertEquals(120, rec.getTtl());
|
||||
assertEquals("1.2.3.4", rec.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuildWithInvalidType() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
RecordEntity.build("id-123", "example.com", "INVALID", 120, "1.2.3.4")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSld() {
|
||||
RecordEntity rec = new RecordEntity();
|
||||
assertNull(rec.getSld());
|
||||
|
||||
rec.setName("sub.example.com");
|
||||
assertEquals("sub", rec.getSld());
|
||||
|
||||
rec.setName("example.com");
|
||||
assertEquals("example", rec.getSld());
|
||||
|
||||
rec.setName("host");
|
||||
assertEquals("host", rec.getSld());
|
||||
|
||||
rec.setName(".dotstart");
|
||||
assertEquals(".dotstart", rec.getSld());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSldWithZoneName() {
|
||||
RecordEntity rec = new RecordEntity();
|
||||
rec.setName("sub.example.com");
|
||||
rec.setZoneName("example.com");
|
||||
assertEquals("sub", rec.getSld());
|
||||
|
||||
rec.setName("my.sub.example.com");
|
||||
rec.setZoneName("example.com");
|
||||
assertEquals("my.sub", rec.getSld());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package codes.thischwa.cf.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class RecordTypeTest {
|
||||
|
||||
@Test
|
||||
void testGetType() {
|
||||
assertEquals("A", RecordType.A.getType());
|
||||
assertEquals("AAAA", RecordType.AAAA.getType());
|
||||
assertEquals("CNAME", RecordType.CNAME.getType());
|
||||
assertEquals("TXT", RecordType.TXT.getType());
|
||||
assertEquals("SRV", RecordType.SRV.getType());
|
||||
assertEquals("LOC", RecordType.LOC.getType());
|
||||
assertEquals("MX", RecordType.MX.getType());
|
||||
assertEquals("NS", RecordType.NS.getType());
|
||||
assertEquals("CAA", RecordType.CAA.getType());
|
||||
assertEquals("CERT", RecordType.CERT.getType());
|
||||
assertEquals("DNSKEY", RecordType.DNSKEY.getType());
|
||||
assertEquals("DS", RecordType.DS.getType());
|
||||
assertEquals("NAPTR", RecordType.NAPTR.getType());
|
||||
assertEquals("SMIMEA", RecordType.SMIMEA.getType());
|
||||
assertEquals("SSHFP", RecordType.SSHFP.getType());
|
||||
assertEquals("TLSA", RecordType.TLSA.getType());
|
||||
assertEquals("URI", RecordType.URI.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
assertEquals("A", RecordType.A.toString());
|
||||
assertEquals("CNAME", RecordType.CNAME.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValueOf() {
|
||||
assertEquals(RecordType.A, RecordType.valueOf("A"));
|
||||
assertEquals(RecordType.CNAME, RecordType.valueOf("CNAME"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package codes.thischwa.cf.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ZoneEntityTest {
|
||||
|
||||
@Test
|
||||
void testZoneEntity() {
|
||||
ZoneEntity zone = new ZoneEntity();
|
||||
zone.setId("zone-id");
|
||||
zone.setName("example.com");
|
||||
zone.setDevelopmentMode(7200);
|
||||
Set<String> ns = Set.of("ns1.cloudflare.com", "ns2.cloudflare.com");
|
||||
zone.setNameServers(ns);
|
||||
zone.setOriginalNameServers(ns);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
zone.setCreatedOn(now);
|
||||
zone.setModifiedOn(now);
|
||||
zone.setActivatedOn(now);
|
||||
zone.setStatus("active");
|
||||
zone.setPaused(false);
|
||||
zone.setType("full");
|
||||
|
||||
assertEquals("zone-id", zone.getId());
|
||||
assertEquals("example.com", zone.getName());
|
||||
assertEquals(7200, zone.getDevelopmentMode());
|
||||
assertEquals(ns, zone.getNameServers());
|
||||
assertEquals(ns, zone.getOriginalNameServers());
|
||||
assertEquals(now, zone.getCreatedOn());
|
||||
assertEquals(now, zone.getModifiedOn());
|
||||
assertEquals(now, zone.getActivatedOn());
|
||||
assertEquals("active", zone.getStatus());
|
||||
assertFalse(zone.getPaused());
|
||||
assertEquals("full", zone.getType());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<configuration debug="true">
|
||||
|
||||
<appender name="current"
|
||||
class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{50} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="current"
|
||||
class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{50} - %msg%n</pattern>
|
||||
</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"/>
|
||||
</root>
|
||||
<root level="debug">
|
||||
<appender-ref ref="current"/>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user