From d1e2a78f24ed11733ed8fd712648a9d17e8e45ca Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 10:33:30 +0100 Subject: [PATCH 01/21] Add GitHub Actions for Maven setup, JUnit reporting, and build analysis with SonarCloud. --- .github/actions/publish-report/action.yml | 35 +++++++++++++ .github/actions/setup-java-maven/action.yml | 43 ++++++++++++++++ .github/workflow/build-and-analyse.yml | 55 +++++++++++++++++++++ pom.xml | 10 +++- 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 .github/actions/publish-report/action.yml create mode 100644 .github/actions/setup-java-maven/action.yml create mode 100644 .github/workflow/build-and-analyse.yml diff --git a/.github/actions/publish-report/action.yml b/.github/actions/publish-report/action.yml new file mode 100644 index 0000000..a5811e4 --- /dev/null +++ b/.github/actions/publish-report/action.yml @@ -0,0 +1,35 @@ +name: Publish JUnit Report (Short Names) +description: Normalize JUnit XML report names and publish a summary-only test report. +inputs: + token: + description: GitHub token for creating the check run. + required: true + report-name: + description: Name shown for the test report. + required: false + default: Summary of JUnit Tests + +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-}" + short="${short%.xml}" + cp "$f" "junit-short/${short}" + done + + - name: Publish Test Report + uses: dorny/test-reporter@v2 + with: + token: ${{ inputs.token }} + name: ${{ inputs.report-name }} + path: "*" + reporter: java-junit + only-summary: true + working-directory: "junit-short" diff --git a/.github/actions/setup-java-maven/action.yml b/.github/actions/setup-java-maven/action.yml new file mode 100644 index 0000000..06dbe9f --- /dev/null +++ b/.github/actions/setup-java-maven/action.yml @@ -0,0 +1,43 @@ +name: "Setup Maven with GitHub Packages" + +description: "Sets up JDK, caches Maven dependencies, and configures GitHub Packages for Maven repositories." + +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 + uses: actions/setup-java@v4 + with: + distribution: ${{ inputs.java-distribution }} + java-version: ${{ inputs.java-version }} + + - name: Cache Maven Repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Set up Maven with GitHub Packages + run: | + echo " + + + github-cloudflaredns + th-schwarz + ${{ secrets.GITHUB_TOKEN }} + + + " > ~/.m2/settings.xml + shell: + bash \ No newline at end of file diff --git a/.github/workflow/build-and-analyse.yml b/.github/workflow/build-and-analyse.yml new file mode 100644 index 0000000..c9598a6 --- /dev/null +++ b/.github/workflow/build-and-analyse.yml @@ -0,0 +1,55 @@ +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@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + + - name: Setup Java and Maven + uses: ./.github/actions/setup-java-maven + + - name: Set up Maven with GitHub Packages + run: | + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml << 'EOF' + + + + github-cloudflaredns + th-schwarz + ${{ secrets.GITHUB_TOKEN }} + + + + EOF + + - name: Build and analyze with SonarCloud + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + run: mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_DynDRest + + - name: Publish Test Report + uses: ./.github/actions/publish-report/ + if: ${{ always() }} + with: + token: ${{ secrets.GITHUB_TOKEN }} + report-name: Summary of JUnit Tests diff --git a/pom.xml b/pom.xml index 7bc7faf..037f23e 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,15 @@ 5.14.2 5.21.0 + + th-schwarz + https://sonarcloud.io + ${file.encoding} + th-schwarz_CloudflareDNS-java + CloudflareDNS-java + develop + src/test/java/**/* + 1.18.20.0 @@ -273,7 +282,6 @@ - From 85f462e002a00208b221cc87b23b81bb93604c34 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 10:41:45 +0100 Subject: [PATCH 02/21] Add GitHub Actions for Maven setup, JUnit reporting, and build analysis with SonarCloud. --- .github/workflow/build-and-analyse.yml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflow/build-and-analyse.yml b/.github/workflow/build-and-analyse.yml index c9598a6..04640df 100644 --- a/.github/workflow/build-and-analyse.yml +++ b/.github/workflow/build-and-analyse.yml @@ -25,27 +25,12 @@ jobs: - name: Setup Java and Maven uses: ./.github/actions/setup-java-maven - - name: Set up Maven with GitHub Packages - run: | - mkdir -p ~/.m2 - cat > ~/.m2/settings.xml << 'EOF' - - - - github-cloudflaredns - th-schwarz - ${{ secrets.GITHUB_TOKEN }} - - - - EOF - - name: Build and analyze with SonarCloud env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} - run: mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_DynDRest + run: mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_CloudflareDNS-java - name: Publish Test Report uses: ./.github/actions/publish-report/ From 4d7deb3f2c51d2b0623ed36b7b017a5dd4c25314 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 10:50:21 +0100 Subject: [PATCH 03/21] Simplify Maven setup by removing unused settings configuration and improving readability of SonarCloud scan step. --- .github/actions/setup-java-maven/action.yml | 16 +--------------- .github/workflow/build-and-analyse.yml | 5 +++-- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/actions/setup-java-maven/action.yml b/.github/actions/setup-java-maven/action.yml index 06dbe9f..fe1d914 100644 --- a/.github/actions/setup-java-maven/action.yml +++ b/.github/actions/setup-java-maven/action.yml @@ -26,18 +26,4 @@ runs: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | - ${{ runner.os }}-maven- - - - name: Set up Maven with GitHub Packages - run: | - echo " - - - github-cloudflaredns - th-schwarz - ${{ secrets.GITHUB_TOKEN }} - - - " > ~/.m2/settings.xml - shell: - bash \ No newline at end of file + ${{ runner.os }}-maven- \ No newline at end of file diff --git a/.github/workflow/build-and-analyse.yml b/.github/workflow/build-and-analyse.yml index 04640df..8e19316 100644 --- a/.github/workflow/build-and-analyse.yml +++ b/.github/workflow/build-and-analyse.yml @@ -21,7 +21,6 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Setup Java and Maven uses: ./.github/actions/setup-java-maven @@ -30,7 +29,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} - run: mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_CloudflareDNS-java + run: + - echo "Running SonarCloud analysis..." + - mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_CloudflareDNS-java - name: Publish Test Report uses: ./.github/actions/publish-report/ From 32618a942bfe77c49b87763d067770942adcd17a Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 11:11:30 +0100 Subject: [PATCH 04/21] Streamline SonarCloud scan step by using multiline syntax in GitHub Actions workflow. --- .github/{workflow => workflows}/build-and-analyse.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename .github/{workflow => workflows}/build-and-analyse.yml (80%) diff --git a/.github/workflow/build-and-analyse.yml b/.github/workflows/build-and-analyse.yml similarity index 80% rename from .github/workflow/build-and-analyse.yml rename to .github/workflows/build-and-analyse.yml index 8e19316..76f91f1 100644 --- a/.github/workflow/build-and-analyse.yml +++ b/.github/workflows/build-and-analyse.yml @@ -29,9 +29,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} - run: - - echo "Running SonarCloud analysis..." - - mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_CloudflareDNS-java + run: | + echo "Running SonarCloud analysis..." + mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_CloudflareDNS-java - name: Publish Test Report uses: ./.github/actions/publish-report/ From dd1052cd75a2dbc07e0414ab8fd8419fe50c929d Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 11:18:55 +0100 Subject: [PATCH 05/21] Update workflow: rename `CF_API_TOKEN` to `API_TOKEN` for consistency --- .github/workflows/build-and-analyse.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-analyse.yml b/.github/workflows/build-and-analyse.yml index 76f91f1..a8952c2 100644 --- a/.github/workflows/build-and-analyse.yml +++ b/.github/workflows/build-and-analyse.yml @@ -28,7 +28,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CF_API_TOKEN: ${{ secrets.API_TOKEN }} run: | echo "Running SonarCloud analysis..." mvn -B -DtestClasspath=src/test/ verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=th-schwarz_CloudflareDNS-java From c857c0d2337a5dbfa48fadc7c2f7c57b1d7bf58d Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 11:43:38 +0100 Subject: [PATCH 06/21] Update README: add SonarCloud badges for quality, security, coverage, LOC, and code smells --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c229358..93452ae 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ [![pipeline-badge](https://ci.codeberg.org/api/badges/16522/status.svg?events=push%2Cmanual%2Cpull_request%2Cpull_request_closed)](https://ci.codeberg.org/repos/16522) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=coverage)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) + ## Preface This project provides a java client for minimalistic access to the Cloudflare API version 4, which is mainly used for From f016067931736d6702e593f9dab8c084752ed8a1 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 11:58:14 +0100 Subject: [PATCH 07/21] Update README: add Codeberg image with link and embed asset in docs --- README.md | 2 ++ docs/codeberg.png | Bin 0 -> 19723 bytes 2 files changed, 2 insertions(+) create mode 100644 docs/codeberg.png diff --git a/README.md b/README.md index 93452ae..bd5a431 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=th-schwarz_CloudflareDNS-java&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=th-schwarz_CloudflareDNS-java) +[![codeberg.png](docs/codeberg.png)](https://codeberg.org/th-schwarz/CloudflareDNS-java) + ## Preface This project provides a java client for minimalistic access to the Cloudflare API version 4, which is mainly used for diff --git a/docs/codeberg.png b/docs/codeberg.png new file mode 100644 index 0000000000000000000000000000000000000000..39465b44c2606d725888d546640f412a7f10f51b GIT binary patch literal 19723 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 literal 0 HcmV?d00001 From 6a11868a0b5ced010fdcd11ff3dea2fc3bec3767 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 12:04:50 +0100 Subject: [PATCH 08/21] Update docs: replace Codeberg image in assets --- docs/codeberg.png | Bin 19723 -> 19865 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/codeberg.png b/docs/codeberg.png index 39465b44c2606d725888d546640f412a7f10f51b..a36522b3fa7caa8641e5d819b054af34374d3b10 100644 GIT binary patch literal 19865 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 From abb9704a876cfcb1fe07afffa0d8fbce0a6b73a8 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 12:53:14 +0100 Subject: [PATCH 09/21] close #13 --- README.md | 8 +- .../java/codes/thischwa/cf/CfDnsClient.java | 92 +++++++++---------- .../java/codes/thischwa/cf/CfRequest.java | 14 +-- .../thischwa/cf/fluent/RecordOperations.java | 18 ++-- .../cf/fluent/RecordOperationsImpl.java | 2 +- .../thischwa/cf/fluent/ZoneOperations.java | 4 +- .../cf/fluent/ZoneOperationsImpl.java | 4 +- .../thischwa/cf/fluent/package-info.java | 12 +-- .../codes/thischwa/cf/model/BatchEntry.java | 4 +- .../thischwa/cf/model/BatchResponse.java | 2 +- .../codes/thischwa/cf/model/RecordEntity.java | 46 +++++----- .../cf/model/RecordMultipleResponse.java | 2 +- .../cf/model/RecordSingleResponse.java | 2 +- .../codes/thischwa/cf/model/RecordType.java | 82 ++++++++--------- .../codes/thischwa/cf/CfClientPenTest.java | 6 +- .../java/codes/thischwa/cf/CfClientTest.java | 28 +++--- 16 files changed, 163 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index bd5a431..15cb21b 100644 --- a/README.md +++ b/README.md @@ -367,7 +367,7 @@ that reduces verbosity and improves code readability. ### Basic Usage ```java -// Create a DNS record +// Create a DNS getRecord client.zone("example.com") .record("api") .create(RecordType.A, "192.168.1.1",60); @@ -381,7 +381,7 @@ List records = client.zone("example.com") List zoneRecords = client.zone("example.com") .list(RecordType.A, RecordType.AAAA); -// Update a DNS record +// Update a DNS getRecord RecordEntity updated = client.zone("example.com") .record("api", RecordType.A) .update("192.168.1.2"); @@ -406,7 +406,7 @@ CfDnsClient client = new CfDnsClientBuilder() .withApiTokenAuth("your-api-token") .build(); -// Create a new record +// Create a new getRecord client.zone("example.com") .record("api") .create(RecordType.A, "192.168.100.1",60); @@ -417,7 +417,7 @@ List records = client.zone("example.com") .get(); System.out.println("IP: "+records.get(0).getContent()); -// Update the record +// Update the getRecord client.zone("example.com") .record("api",RecordType.A) .update("192.168.100.2"); diff --git a/src/main/java/codes/thischwa/cf/CfDnsClient.java b/src/main/java/codes/thischwa/cf/CfDnsClient.java index 4a1490a..044853d 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClient.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClient.java @@ -41,11 +41,11 @@ import org.jetbrains.annotations.Nullable; * * // Retrieve records of a subdomain * List<RecordEntity> records = cfDnsClient.recordList(zone, "sld"); - * records.forEach(record -> - * System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent()) + * records.forEach(getRecord -> + * System.out.println("Record Type: " + getRecord.getType() + ", Value: " + getRecord.getContent()) * ); * - * // Create a record for the subdomain "api" + * // Create a getRecord for the subdomain "api" * RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10"); * System.out.println("Created Record ID: " + created.getId()); * @@ -123,7 +123,7 @@ public class CfDnsClient extends CfBasicHttpClient { *

Example: *


    * client.zone("example.com")
-   *       .record("api")
+   *       .getRecord("api")
    *       .create(RecordType.A, "192.168.1.1", 60);
    * 
* @@ -192,11 +192,11 @@ public class CfDnsClient extends CfBasicHttpClient { /** * Retrieves DNS records for the specified second-level domain (SLD) within a zone. - * Optionally filters by one or more DNS record types. + * Optionally filters by one or more DNS getRecord types. * * @param zone The zone entity containing information about the domain zone. * @param sld The second-level domain (SLD) for which to retrieve DNS records. - * @param types Optional parameter specifying one or more DNS record types to filter the results. + * @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 @@ -210,7 +210,7 @@ public class CfDnsClient extends CfBasicHttpClient { /** - * Retrieves all record entities for a specific second-level domain (SLD) within a given DNS + * Retrieves all getRecord entities for a specific second-level domain (SLD) within a given DNS * zone using the provided paging request parameters. * * @param zone The DNS zone entity for which the SLD records are to be fetched. @@ -231,10 +231,10 @@ public class CfDnsClient extends CfBasicHttpClient { /** * Retrieves a list of all DNS records for a given zone. - * Optionally filters by one or more DNS record types. + * 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 record types to filter the results. + * @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 */ @@ -248,17 +248,17 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * Creates a new DNS record for a given second-level domain (SLD) within the specified zone. + * 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 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 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 { @@ -267,14 +267,14 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * 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, @@ -284,13 +284,13 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * 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) @@ -305,11 +305,11 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * 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. */ @@ -324,11 +324,11 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * 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. */ @@ -341,12 +341,12 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * 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) @@ -362,11 +362,11 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * 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 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. */ @@ -377,7 +377,7 @@ public class CfDnsClient extends CfBasicHttpClient { try { recs = recordList(zone, sld, recordTypes); } catch (CloudflareNotFoundException e) { - log.trace("No record of type {} found for domain {}.", recordTypes, fqdn); + log.trace("No getRecord of type {} found for domain {}.", recordTypes, fqdn); return; } for (RecordEntity rec : recs) { @@ -385,13 +385,13 @@ public class CfDnsClient extends CfBasicHttpClient { recordDelete(zone, rec); log.info("Record {} of type {} successful deleted.", fqdn, recordTypes); } catch (CloudflareApiException e) { - log.error("Failed to delete record {} of type {} for zone {}: {}", fqdn, recordTypes, zone.getName(), e.getMessage()); + log.error("Failed to delete getRecord {} of type {} for zone {}: {}", fqdn, recordTypes, zone.getName(), e.getMessage()); } } } /** - * Processes a batch of DNS record operations (POST, PUT, PATCH, DELETE) for a specified zone. + * 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. * @@ -407,7 +407,7 @@ public class CfDnsClient extends CfBasicHttpClient { @Nullable List patchRecords, @Nullable List deleteRecords) throws CloudflareApiException { BatchEntry batchEntry = new BatchEntry(); - // build 'clean' record entries + // build 'clean' getRecord entries if (postRecords != null) { batchEntry.setPosts(cleanRecordsForPostOrPut(postRecords)); } diff --git a/src/main/java/codes/thischwa/cf/CfRequest.java b/src/main/java/codes/thischwa/cf/CfRequest.java index dcf0fe0..cd7f3e9 100644 --- a/src/main/java/codes/thischwa/cf/CfRequest.java +++ b/src/main/java/codes/thischwa/cf/CfRequest.java @@ -27,20 +27,20 @@ public enum CfRequest { */ RECORD_LIST("/zones/%s/dns_records"), /** - * Represents the API endpoint path for creating a new DNS record within a specific DNS zone. The + * Represents the API endpoint path for creating a new DNS getRecord within a specific DNS zone. The * endpoint path includes a placeholder for the zone identifier, which needs to be provided to * construct the complete path. */ RECORD_CREATE("/zones/%s/dns_records"), /** - * Represents the API endpoint path for retrieving information about a DNS record within a + * 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 record name, which need to be provided to construct the complete path. + * and the getRecord name, which need to be provided to construct the complete path. */ RECORD_INFO_NAME("/zones/%s/dns_records?name=%s"), /** - * Represents the API endpoint path for updating an existing DNS 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"), @@ -52,8 +52,8 @@ public enum CfRequest { */ RECORD_BATCH("/zones/%s/dns_records/batch"), /** - * Represents the API endpoint path for deleting an existing DNS record within a specific DNS - * zone. The endpoint path includes placeholders for the zone identifier and the record + * 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"); diff --git a/src/main/java/codes/thischwa/cf/fluent/RecordOperations.java b/src/main/java/codes/thischwa/cf/fluent/RecordOperations.java index 579300b..281d662 100644 --- a/src/main/java/codes/thischwa/cf/fluent/RecordOperations.java +++ b/src/main/java/codes/thischwa/cf/fluent/RecordOperations.java @@ -6,7 +6,7 @@ import codes.thischwa.cf.model.RecordType; import java.util.List; /** - * Fluent interface for record-level operations. + * Fluent interface for getRecord-level operations. * Provides a chainable API for CRUD operations on DNS records. */ public interface RecordOperations { @@ -20,29 +20,29 @@ public interface RecordOperations { List get() throws CloudflareApiException; /** - * Creates a new DNS record with the specified parameters. + * Creates a new DNS getRecord with the specified parameters. * - * @param type the DNS record type (e.g., A, AAAA, CNAME) - * @param content the content of the DNS record (e.g., IP address) + * @param 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 record + * @throws CloudflareApiException if an error occurs while creating the getRecord */ RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException; /** - * Updates an existing DNS record with new content. + * Updates an existing DNS getRecord with new content. * - * @param newContent the new content for the DNS record + * @param newContent the new content for the DNS getRecord * @return the updated RecordEntity - * @throws CloudflareApiException if an error occurs while updating the record + * @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 record types to delete + * @param types the DNS getRecord types to delete * @throws CloudflareApiException if an error occurs while deleting records */ void delete(RecordType... types) throws CloudflareApiException; diff --git a/src/main/java/codes/thischwa/cf/fluent/RecordOperationsImpl.java b/src/main/java/codes/thischwa/cf/fluent/RecordOperationsImpl.java index da9a5b7..eebe8d6 100644 --- a/src/main/java/codes/thischwa/cf/fluent/RecordOperationsImpl.java +++ b/src/main/java/codes/thischwa/cf/fluent/RecordOperationsImpl.java @@ -9,7 +9,7 @@ import java.util.List; import org.jetbrains.annotations.Nullable; /** - * Implementation of RecordOperations for fluent API access to record-level operations. + * Implementation of RecordOperations for fluent API access to getRecord-level operations. */ public class RecordOperationsImpl implements RecordOperations { diff --git a/src/main/java/codes/thischwa/cf/fluent/ZoneOperations.java b/src/main/java/codes/thischwa/cf/fluent/ZoneOperations.java index 5056351..2c8c21e 100644 --- a/src/main/java/codes/thischwa/cf/fluent/ZoneOperations.java +++ b/src/main/java/codes/thischwa/cf/fluent/ZoneOperations.java @@ -17,7 +17,7 @@ public interface ZoneOperations { * @return a RecordOperations instance for chaining record-specific operations * @throws CloudflareApiException if the zone cannot be found or accessed */ - RecordOperations record(String sld) throws CloudflareApiException; + RecordOperations getRecord(String sld) throws CloudflareApiException; /** * Selects a record with specific types within the zone for further operations. @@ -27,7 +27,7 @@ public interface ZoneOperations { * @return a RecordOperations instance for chaining record-specific operations * @throws CloudflareApiException if the zone cannot be found or accessed */ - RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException; + RecordOperations getRecord(String sld, @Nullable RecordType... types) throws CloudflareApiException; /** * Lists all DNS records within the zone, optionally filtered by types. diff --git a/src/main/java/codes/thischwa/cf/fluent/ZoneOperationsImpl.java b/src/main/java/codes/thischwa/cf/fluent/ZoneOperationsImpl.java index 97bdae4..fd78f28 100644 --- a/src/main/java/codes/thischwa/cf/fluent/ZoneOperationsImpl.java +++ b/src/main/java/codes/thischwa/cf/fluent/ZoneOperationsImpl.java @@ -28,12 +28,12 @@ public class ZoneOperationsImpl implements ZoneOperations { } @Override - public RecordOperations record(String sld) throws CloudflareApiException { + public RecordOperations getRecord(String sld) throws CloudflareApiException { return new RecordOperationsImpl(client, zone, sld, null); } @Override - public RecordOperations record(String sld, @Nullable RecordType... types) throws CloudflareApiException { + public RecordOperations getRecord(String sld, @Nullable RecordType... types) throws CloudflareApiException { return new RecordOperationsImpl(client, zone, sld, types); } diff --git a/src/main/java/codes/thischwa/cf/fluent/package-info.java b/src/main/java/codes/thischwa/cf/fluent/package-info.java index 8478e70..511ec69 100644 --- a/src/main/java/codes/thischwa/cf/fluent/package-info.java +++ b/src/main/java/codes/thischwa/cf/fluent/package-info.java @@ -6,24 +6,24 @@ * *

Example usage: *


- * // Create a DNS record
+ * // Create a DNS getRecord
  * client.zone("example.com")
- *       .record("api")
+ *       .getRecord("api")
  *       .create(RecordType.A, "192.168.1.1", 60);
  *
  * // Get DNS records
  * List<RecordEntity> records = client.zone("example.com")
- *                                      .record("www", RecordType.A)
+ *                                      .getRecord("www", RecordType.A)
  *                                      .get();
  *
- * // Update a DNS record
+ * // Update a DNS getRecord
  * client.zone("example.com")
- *       .record("api", RecordType.A)
+ *       .getRecord("api", RecordType.A)
  *       .update("192.168.1.2");
  *
  * // Delete DNS records
  * client.zone("example.com")
- *       .record("old-service")
+ *       .getRecord("old-service")
  *       .delete(RecordType.A, RecordType.AAAA);
  * 
*/ diff --git a/src/main/java/codes/thischwa/cf/model/BatchEntry.java b/src/main/java/codes/thischwa/cf/model/BatchEntry.java index bee8682..dc93e02 100644 --- a/src/main/java/codes/thischwa/cf/model/BatchEntry.java +++ b/src/main/java/codes/thischwa/cf/model/BatchEntry.java @@ -5,11 +5,11 @@ import lombok.Data; import lombok.EqualsAndHashCode; /** - * Represents a batch entry containing different types of operations on record entities. + * Represents a batch entry containing different types of operations on getRecord entities. * *

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

    *
  • patches: A list of {@link RecordEntity} objects representing partial updates to existing records. diff --git a/src/main/java/codes/thischwa/cf/model/BatchResponse.java b/src/main/java/codes/thischwa/cf/model/BatchResponse.java index d66b892..9ba4a0c 100644 --- a/src/main/java/codes/thischwa/cf/model/BatchResponse.java +++ b/src/main/java/codes/thischwa/cf/model/BatchResponse.java @@ -5,7 +5,7 @@ package codes.thischwa.cf.model; * *

    This class is used for API responses where the primary result is a batch entry, * which includes collections of operations such as patches, posts, puts, and deletes - * performed on DNS record entities. + * performed on DNS getRecord entities. * *

    Extends {@code AbstractSingleResponse} with {@code BatchEntry} as the generic type, * ensuring that the response result is a batch of operations. diff --git a/src/main/java/codes/thischwa/cf/model/RecordEntity.java b/src/main/java/codes/thischwa/cf/model/RecordEntity.java index 7fb32d3..5f18daa 100644 --- a/src/main/java/codes/thischwa/cf/model/RecordEntity.java +++ b/src/main/java/codes/thischwa/cf/model/RecordEntity.java @@ -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. * *

    Attributes defined in this class include: * *

      - *
    • DNS record type such as "A" or "CNAME". - *
    • Name of the DNS record. - *
    • Content of the DNS record, such as an IP address. - *
    • Flags indicating whether the record is proxiable or proxied. - *
    • TTL (Time-To-Live) for the DNS record. - *
    • A locked status to indicate the immutability of the record. + *
    • DNS getRecord type such as "A" or "CNAME". + *
    • Name of the DNS getRecord. + *
    • Content of the DNS getRecord, such as an IP address. + *
    • Flags indicating whether the getRecord is proxiable or proxied. + *
    • TTL (Time-To-Live) for the DNS getRecord. + *
    • A locked status to indicate the immutability of the getRecord. *
    • Zone-specific metadata including zone ID and name. *
    • Timestamps for creation and modification. *
    * - *

    Provides a static factory method {@code build} for creating a DNS record with specific + *

    Provides a static factory method {@code build} for creating a DNS getRecord with specific * attributes. */ @EqualsAndHashCode(callSuper = true) @@ -45,7 +45,7 @@ public class RecordEntity extends AbstractEntity { /** * 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. */ @@ -56,10 +56,10 @@ 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 content 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 content) { @@ -74,8 +74,8 @@ public class RecordEntity extends AbstractEntity { /** * Builds and returns a {@link RecordEntity} instance with the specified ID and content. * - * @param id the unique identifier for the DNS record - * @param content the content of the DNS record, typically an IP address or other record data + * @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) { @@ -88,11 +88,11 @@ public class RecordEntity extends AbstractEntity { /** * Builds and returns a {@link RecordEntity} instance with the specified attributes. * - * @param id the unique identifier for the DNS record - * @param name the name of the DNS record - * @param type the type of the DNS record, represented as a string (e.g., "A", "CNAME") - * @param ttl the time-to-live (TTL) value for the DNS record - * @param content the content of the DNS record, typically an IP address or other record data + * @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 */ @@ -101,7 +101,7 @@ public class RecordEntity extends AbstractEntity { try { recordType = RecordType.valueOf(type); } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: " + throw new IllegalArgumentException("Invalid getRecord type: " + type + ". Must be one of: " + java.util.Arrays.toString(RecordType.values()), e); } RecordEntity rec = new RecordEntity(); @@ -114,11 +114,11 @@ public class RecordEntity extends AbstractEntity { } /** - * Retrieves the short name (subdomain) of the DNS record. + * 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 record (substring before the first dot), + * @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() { diff --git a/src/main/java/codes/thischwa/cf/model/RecordMultipleResponse.java b/src/main/java/codes/thischwa/cf/model/RecordMultipleResponse.java index 466e06d..5ea8672 100644 --- a/src/main/java/codes/thischwa/cf/model/RecordMultipleResponse.java +++ b/src/main/java/codes/thischwa/cf/model/RecordMultipleResponse.java @@ -9,7 +9,7 @@ public class RecordMultipleResponse extends AbstractMultipleResponseThis class represents a response containing multiple DNS record entities from the + *

    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. */ diff --git a/src/main/java/codes/thischwa/cf/model/RecordSingleResponse.java b/src/main/java/codes/thischwa/cf/model/RecordSingleResponse.java index 5b1b9a0..48d0a21 100644 --- a/src/main/java/codes/thischwa/cf/model/RecordSingleResponse.java +++ b/src/main/java/codes/thischwa/cf/model/RecordSingleResponse.java @@ -11,7 +11,7 @@ public class RecordSingleResponse extends AbstractSingleResponse { * *

    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() { diff --git a/src/main/java/codes/thischwa/cf/model/RecordType.java b/src/main/java/codes/thischwa/cf/model/RecordType.java index bff7a80..5d79f53 100644 --- a/src/main/java/codes/thischwa/cf/model/RecordType.java +++ b/src/main/java/codes/thischwa/cf/model/RecordType.java @@ -3,86 +3,86 @@ package codes.thischwa.cf.model; import lombok.Getter; /** - * Enum representing various DNS record types. + * Enum representing various DNS getRecord types. * - *

    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 + *

    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. * - *

    The "A" record type is used to map a domain name to an IPv4 address. + *

    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. * - *

    The "AAAA" record type maps a domain name to an IPv6 address. + *

    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. * - *

    The "CAA" record type is used to specify which certificate authorities (CAs) are allowed to + *

    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. * - *

    The "CERT" record type is used to store certificates and related certificate revocation + *

    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. * - *

    The "CNAME" record type is used to alias one domain name to another. + *

    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. * - *

    The "DNSKEY" record type is used for storing public keys in DNS, as part of the DNS Security + *

    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. * - *

    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 + *

    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. * - *

    The "HTTPS" record type is used to specify information about the HTTP/3 and related services + *

    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. * - *

    The "LOC" record type is used to store geographical location information for a domain, + *

    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. * - *

    The "MX" record type is used to specify the mail servers responsible for receiving email + *

    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. * *

    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. * *

    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. * - *

    The `PTR` value specifically refers to a "pointer record" in the DNS system, which is + *

    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. * - *

    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 + *

    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. * *

    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. * - *

    This constant may be used to identify and work with SRV record types in various DNS-related + *

    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. * - *

    This DNS record type provides a mechanism for verifying the authenticity of an SSH server + *

    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. * - *

    The SVCB record is a DNS resource record used to indicate alternative endpoints or specific + *

    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. * - *

    The TLSA record is used to associate a TLS server certificate or public key with the domain + *

    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. * - *

    The TXT DNS record type is commonly used to store text-based information for various + *

    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). */ diff --git a/src/test/java/codes/thischwa/cf/CfClientPenTest.java b/src/test/java/codes/thischwa/cf/CfClientPenTest.java index 82f0186..a2bccb5 100644 --- a/src/test/java/codes/thischwa/cf/CfClientPenTest.java +++ b/src/test/java/codes/thischwa/cf/CfClientPenTest.java @@ -66,7 +66,7 @@ public class CfClientPenTest { } @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.zoneGet(ZONE_STR); @@ -81,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")); diff --git a/src/test/java/codes/thischwa/cf/CfClientTest.java b/src/test/java/codes/thischwa/cf/CfClientTest.java index c1c7305..0201359 100644 --- a/src/test/java/codes/thischwa/cf/CfClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfClientTest.java @@ -127,7 +127,7 @@ 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()); @@ -146,7 +146,7 @@ public class CfClientTest { 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"); List aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA); @@ -166,7 +166,7 @@ public class CfClientTest { } else if (Objects.equals(re.getType(), RecordType.AAAA.getType())) { assertEquals("2a0a:4cc0:c0:2e4::1", re.getContent()); } else { - fail(String.format("Unexpected record type: %s", re.getType())); + fail(String.format("Unexpected getRecord type: %s", re.getType())); } } @@ -191,7 +191,7 @@ public class CfClientTest { assertFalse(fluentList.isEmpty()); assertTrue(fluentList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId()))); - // update AAAA record + // update AAAA getRecord createdRe2.setContent("2a0a:4cc0:c0:2e4::2"); client.recordUpdate(z, createdRe2); aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA); @@ -199,18 +199,18 @@ public class CfClientTest { r = aaaaRecords.get(0); assertEquals("2a0a:4cc0:c0:2e4::2", r.getContent()); - // verify A record still intact + // 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.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.recordList(z, randomSld, RecordType.A)); @@ -226,7 +226,7 @@ public class CfClientTest { void testRecordEntityInvalidType() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> RecordEntity.build("id123", "example.com", "INVALID_TYPE", 60, "192.168.1.1")); - assertTrue(exception.getMessage().contains("Invalid record type: INVALID_TYPE")); + assertTrue(exception.getMessage().contains("Invalid getRecord type: INVALID_TYPE")); assertTrue(exception.getMessage().contains("Must be one of:")); } @@ -373,7 +373,7 @@ public class CfClientTest { try { // Test fluent create RecordEntity created = client.zone(ZONE_STR) - .record(fluentSld) + .getRecord(fluentSld) .create(RecordType.A, "192.168.100.1", TTL); assertNotNull(created.getId()); @@ -382,7 +382,7 @@ public class CfClientTest { // Test fluent get List records = client.zone(ZONE_STR) - .record(fluentSld, RecordType.A) + .getRecord(fluentSld, RecordType.A) .get(); assertEquals(1, records.size()); @@ -390,18 +390,18 @@ public class CfClientTest { // Test fluent update RecordEntity updated = client.zone(ZONE_STR) - .record(fluentSld, RecordType.A) + .getRecord(fluentSld, RecordType.A) .update("192.168.100.2"); assertEquals("192.168.100.2", updated.getContent()); // Test fluent delete client.zone(ZONE_STR) - .record(fluentSld) + .getRecord(fluentSld) .delete(RecordType.A); assertThrows(CloudflareNotFoundException.class, - () -> client.zone(ZONE_STR).record(fluentSld, RecordType.A).get()); + () -> client.zone(ZONE_STR).getRecord(fluentSld, RecordType.A).get()); } finally { try { @@ -425,7 +425,7 @@ public class CfClientTest { assertNotNull(groupedRecords, "Resulting map should not be null."); assertEquals(2, groupedRecords.size(), "The grouping should result in 2 FQDN keys."); assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records."); - assertEquals(1, groupedRecords.get("sub.example.com.").size(), "The key 'sub.example.com.' should have 1 record."); + assertEquals(1, groupedRecords.get("sub.example.com.").size(), "The key 'sub.example.com.' should have 1 getRecord."); } @Test From 092412cf9247f3f3800d3d1244857d8960805fcc Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 17:53:56 +0100 Subject: [PATCH 10/21] fixed docu --- src/main/java/codes/thischwa/cf/CfDnsClient.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/codes/thischwa/cf/CfDnsClient.java b/src/main/java/codes/thischwa/cf/CfDnsClient.java index 044853d..d482fed 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClient.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClient.java @@ -41,11 +41,11 @@ import org.jetbrains.annotations.Nullable; * * // Retrieve records of a subdomain * List<RecordEntity> records = cfDnsClient.recordList(zone, "sld"); - * records.forEach(getRecord -> - * System.out.println("Record Type: " + getRecord.getType() + ", Value: " + getRecord.getContent()) + * records.forEach(record -> + * System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent()) * ); * - * // Create a getRecord for the subdomain "api" + * // Create a record for the subdomain "api" * RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10"); * System.out.println("Created Record ID: " + created.getId()); * From aaae19f7834fc47eda80f42b12d8edf36a1e5511 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Sun, 8 Mar 2026 18:04:07 +0100 Subject: [PATCH 11/21] Update README: document breaking change in `client.zone().record()` renaming to `client.zone().getRecord()` --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 15cb21b..e8f9ac8 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ The dependency is: ## Changelog +- 0.4.0-SNAPSHOT: + - **Breaking Change**: renamed `client.zone().record()` to `client.zone().getRecord()` - 0.3.0: - **Breaking Change**: - **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`) From 8d2fc74d04330d5c3cd1afd6de38307292ea53dc Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 09:58:58 +0100 Subject: [PATCH 12/21] Add JUnit tests for model classes: `RecordEntityTest`, `ZoneEntityTest`, `BatchEntryTest`, `RecordTypeTest`, and update `PagingRequestTest`. --- .../thischwa/cf/model/BatchEntryTest.java | 47 +++++++++++++ .../thischwa/cf/model/PagingRequestTest.java | 26 ++++++- .../thischwa/cf/model/RecordEntityTest.java | 70 +++++++++++++++++++ .../thischwa/cf/model/RecordTypeTest.java | 40 +++++++++++ .../thischwa/cf/model/ZoneEntityTest.java | 41 +++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 src/test/java/codes/thischwa/cf/model/BatchEntryTest.java create mode 100644 src/test/java/codes/thischwa/cf/model/RecordEntityTest.java create mode 100644 src/test/java/codes/thischwa/cf/model/RecordTypeTest.java create mode 100644 src/test/java/codes/thischwa/cf/model/ZoneEntityTest.java diff --git a/src/test/java/codes/thischwa/cf/model/BatchEntryTest.java b/src/test/java/codes/thischwa/cf/model/BatchEntryTest.java new file mode 100644 index 0000000..e46bb32 --- /dev/null +++ b/src/test/java/codes/thischwa/cf/model/BatchEntryTest.java @@ -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 patches = new ArrayList<>(); + List posts = new ArrayList<>(); + List puts = new ArrayList<>(); + List 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 patches = List.of(new RecordEntity()); + entry1.setPatches(patches); + assertNotEquals(entry1, entry2); + + entry2.setPatches(patches); + assertEquals(entry1, entry2); + + assertTrue(entry1.toString().contains("patches=")); + } +} diff --git a/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java b/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java index 8c6512f..0e88c79 100644 --- a/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java +++ b/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java @@ -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); } @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); } + @Test + void testGetPagingParams() { + PagingRequest request = PagingRequest.of(2, 50); + java.util.Map 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()); + } + } diff --git a/src/test/java/codes/thischwa/cf/model/RecordEntityTest.java b/src/test/java/codes/thischwa/cf/model/RecordEntityTest.java new file mode 100644 index 0000000..4509b7d --- /dev/null +++ b/src/test/java/codes/thischwa/cf/model/RecordEntityTest.java @@ -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()); + } +} diff --git a/src/test/java/codes/thischwa/cf/model/RecordTypeTest.java b/src/test/java/codes/thischwa/cf/model/RecordTypeTest.java new file mode 100644 index 0000000..9292aae --- /dev/null +++ b/src/test/java/codes/thischwa/cf/model/RecordTypeTest.java @@ -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")); + } +} diff --git a/src/test/java/codes/thischwa/cf/model/ZoneEntityTest.java b/src/test/java/codes/thischwa/cf/model/ZoneEntityTest.java new file mode 100644 index 0000000..08d1abc --- /dev/null +++ b/src/test/java/codes/thischwa/cf/model/ZoneEntityTest.java @@ -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 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()); + } +} From be409139d77957a2b69cc153e9d31ee40ffbac9c Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 14:50:34 +0100 Subject: [PATCH 13/21] issue #15 Add paging support in `recordList` API, update tests, and streamline query parameter names. --- .../java/codes/thischwa/cf/CfDnsClient.java | 59 ++++++++------ .../java/codes/thischwa/cf/CfRequest.java | 13 ++- .../thischwa/cf/model/PagingRequest.java | 4 +- .../java/codes/thischwa/cf/CfClientTest.java | 81 +++++++++++++++++++ .../java/codes/thischwa/cf/CfRequestTest.java | 4 +- .../thischwa/cf/model/PagingRequestTest.java | 4 +- src/test/resources/logback-test.xml | 2 +- 7 files changed, 128 insertions(+), 39 deletions(-) diff --git a/src/main/java/codes/thischwa/cf/CfDnsClient.java b/src/main/java/codes/thischwa/cf/CfDnsClient.java index d482fed..c51605d 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClient.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClient.java @@ -116,7 +116,7 @@ public class CfDnsClient extends CfBasicHttpClient { } /** - * Provides fluent API access to operations on a specific zone. + * Provides fluent API access to operations in a specific zone. * This method returns a ZoneOperations interface that allows chaining operations * on DNS records within the specified zone. * @@ -177,6 +177,33 @@ public class CfDnsClient extends CfBasicHttpClient { return response.getResult().get(0); } + /** + * Retrieves a list of DNS records for a specified zone, with optional paging support. + * + * @param zone The zone entity containing information about the target zone. + * @return A list of RecordEntity objects representing the DNS records of the specified zone. + * @throws CloudflareApiException If an error occurs during the API request or response processing. + */ + public List recordList(ZoneEntity zone) throws CloudflareApiException { + return recordList(zone, (PagingRequest) null); + } + + /** + * Retrieves a list of DNS records for a specified zone, with optional paging support. + * + * @param zone The zone entity containing information about the target zone. + * @param pagingRequest The paging request containing parameters such as page size and number. + * @return A list of RecordEntity objects representing the DNS records of the specified zone. + * @throws CloudflareApiException If an error occurs during the API request or response processing. + */ + public List recordList(ZoneEntity zone, @Nullable PagingRequest pagingRequest) throws CloudflareApiException { + PagingRequest pr = pagingRequest == null ? PagingRequest.defaultPaging() : pagingRequest; + String endpoint = pr.addQueryString(CfRequest.RECORD_LIST.buildPath(zone.getId())); + RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class); + checkResponse(resp); + return resp.getResult(); + } + /** * Retrieves DNS records for the specified second-level domain (SLD) within a zone. * @@ -203,35 +230,17 @@ public class CfDnsClient extends CfBasicHttpClient { */ public List recordList(ZoneEntity zone, String sld, @Nullable RecordType... types) throws CloudflareApiException { - PagingRequest pagingRequest = PagingRequest.defaultPaging(); - List recs = recordList(zone, sld, pagingRequest); - return filterAndSetZoneRecords(zone, types, recs); - } - - - /** - * Retrieves all getRecord entities for a specific second-level domain (SLD) within a given DNS - * zone using the provided paging request parameters. - * - * @param zone The DNS zone entity for which the SLD records are to be fetched. - * @param sld The second-level domain name for which the records are retrieved. - * @param pagingRequest The paging request. - * @return A list of {@code RecordEntity} associated with the desired SLD. - * @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API. - */ - public List recordList(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)); + String endpoint = CfRequest.RECORD_LIST_NAME.buildPath(zone.getId(), fqdn); RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class); - checkResponse(resp); - return resp.getResult(); + checkResponse(resp, false); + List 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. + * 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. @@ -438,7 +447,7 @@ public class CfDnsClient extends CfBasicHttpClient { Set allowedTypes = new HashSet<>(Arrays.asList(types)); filtered = recs.stream() .filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType()))) - .collect(Collectors.toList());; + .collect(Collectors.toList()); } else { filtered = new ArrayList<>(recs); } diff --git a/src/main/java/codes/thischwa/cf/CfRequest.java b/src/main/java/codes/thischwa/cf/CfRequest.java index cd7f3e9..c2573de 100644 --- a/src/main/java/codes/thischwa/cf/CfRequest.java +++ b/src/main/java/codes/thischwa/cf/CfRequest.java @@ -26,25 +26,24 @@ public enum CfRequest { * 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 getRecord within a - * specific DNS zone by its name. The endpoint path includes placeholders for the zone identifier - * and the getRecord name, which need to be provided to construct the complete path. - */ - RECORD_INFO_NAME("/zones/%s/dns_records?name=%s"), /** * Represents the API endpoint path for updating an existing DNS getRecord within a specific DNS * 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 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. diff --git a/src/main/java/codes/thischwa/cf/model/PagingRequest.java b/src/main/java/codes/thischwa/cf/model/PagingRequest.java index ac061a3..773d002 100644 --- a/src/main/java/codes/thischwa/cf/model/PagingRequest.java +++ b/src/main/java/codes/thischwa/cf/model/PagingRequest.java @@ -25,7 +25,7 @@ public class PagingRequest { * Default page size for retrieving all records in a single request. * Set to a very high value to effectively disable pagination when fetching all records. */ - private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 5_000_000; + private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 1000; private int page; private int perPage; @@ -77,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; } } diff --git a/src/test/java/codes/thischwa/cf/CfClientTest.java b/src/test/java/codes/thischwa/cf/CfClientTest.java index 0201359..75e06dd 100644 --- a/src/test/java/codes/thischwa/cf/CfClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfClientTest.java @@ -9,14 +9,18 @@ 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; @@ -91,6 +95,19 @@ public class CfClientTest { } } + @Test + void testZoneList() throws CloudflareApiException { + List 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 zList = client.zoneList(); @@ -445,4 +462,68 @@ public class CfClientTest { } + @Test + void testPaging() throws Exception { + ZoneEntity zone = client.zoneGet(ZONE_STR); + String pagingSld = "paging-" + System.currentTimeMillis(); + + try { + int existingCount = 0; + try { + List 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 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 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 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 page3Records = client.recordList(zone, page3Request); + assertEquals(2, page3Records.size(), "Third page should contain 2 records"); + + // Verify no overlap between pages + List page1Ids = page1Records.stream().map(RecordEntity::getId).toList(); + List page2Ids = page2Records.stream().map(RecordEntity::getId).toList(); + List page3Ids = page3Records.stream().map(RecordEntity::getId).toList(); + Set generatedRecordIds = new HashSet<>(page1Ids); + generatedRecordIds.addAll(page2Ids); + generatedRecordIds.addAll(page3Ids); + assertEquals(createdRecords.size(), generatedRecordIds.size()); + + // Verify our created records are in the zone + List allRecords = client.recordList(zone); + Set 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 */ } + } + } + } diff --git a/src/test/java/codes/thischwa/cf/CfRequestTest.java b/src/test/java/codes/thischwa/cf/CfRequestTest.java index 1990c2c..14aa061 100644 --- a/src/test/java/codes/thischwa/cf/CfRequestTest.java +++ b/src/test/java/codes/thischwa/cf/CfRequestTest.java @@ -27,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); } @@ -45,7 +45,7 @@ public class CfRequestTest { @Test public void testBuildRecordInfo() { - String result = CfRequest.RECORD_INFO_NAME.buildPath("zone123", "sld.domain.com"); + String result = CfRequest.RECORD_LIST_NAME.buildPath("zone123", "sld.domain.com"); assertEquals("/zones/zone123/dns_records?name=sld.domain.com", result); } diff --git a/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java b/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java index 0e88c79..f399942 100644 --- a/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java +++ b/src/test/java/codes/thischwa/cf/model/PagingRequestTest.java @@ -9,13 +9,13 @@ public class PagingRequestTest { @Test void testBuildPath() { String result = PagingRequest.defaultPaging().addQueryString("/zones"); - assertEquals("/zones?page=1&perPage=5000000", result); + assertEquals("/zones?page=1&per_page=1000", result); } @Test 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 diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 42dc8c6..f272639 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -1,5 +1,5 @@ - + From 6e71ba266a0ade2751b5b082227b17c095efb5ac Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 15:00:35 +0100 Subject: [PATCH 14/21] reduce code smells --- src/main/java/codes/thischwa/cf/CfDnsClient.java | 5 ++--- .../java/codes/thischwa/cf/CfDnsClientBuilder.java | 13 ------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main/java/codes/thischwa/cf/CfDnsClient.java b/src/main/java/codes/thischwa/cf/CfDnsClient.java index c51605d..f159a62 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClient.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClient.java @@ -219,7 +219,7 @@ public class CfDnsClient extends CfBasicHttpClient { /** * Retrieves DNS records for the specified second-level domain (SLD) within a zone. - * Optionally filters by one or more DNS getRecord types. + * Optionally, filters by one or more DNS getRecord types. * * @param zone The zone entity containing information about the domain zone. * @param sld The second-level domain (SLD) for which to retrieve DNS records. @@ -446,8 +446,7 @@ public class CfDnsClient extends CfBasicHttpClient { if (types != null && types.length > 0) { Set allowedTypes = new HashSet<>(Arrays.asList(types)); filtered = recs.stream() - .filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType()))) - .collect(Collectors.toList()); + .filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType()))).toList(); } else { filtered = new ArrayList<>(recs); } diff --git a/src/main/java/codes/thischwa/cf/CfDnsClientBuilder.java b/src/main/java/codes/thischwa/cf/CfDnsClientBuilder.java index bec5da4..c779b68 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClientBuilder.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClientBuilder.java @@ -22,19 +22,6 @@ public class CfDnsClientBuilder { @Nullable private String baseUrl; - /** - * Constructs a new instance of `CfDnsClientBuilder`. - * - *

    This class serves as a builder for creating and configuring instances of a CfDnsClient. It provides - * a fluent API to set various optional configurations, such as API authentication methods and base - * URL, before constructing the client. - * - *

    By using this constructor, you can initiate the building process with default settings, which can - * later be overridden using the provided builder methods. - */ - public CfDnsClientBuilder() { - } - /** * Configures whether an exception should be thrown when an empty result is encountered * during operations performed by the `CfDnsClient`. From 66a5d489277609c67f10fc0b1c2100d015ce9bb2 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 15:10:52 +0100 Subject: [PATCH 15/21] reduce code smells --- .../thischwa/cf/CloudflareNotFoundException.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/codes/thischwa/cf/CloudflareNotFoundException.java b/src/main/java/codes/thischwa/cf/CloudflareNotFoundException.java index ce744bc..c52e592 100644 --- a/src/main/java/codes/thischwa/cf/CloudflareNotFoundException.java +++ b/src/main/java/codes/thischwa/cf/CloudflareNotFoundException.java @@ -18,17 +18,4 @@ public class CloudflareNotFoundException extends CloudflareApiException { 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); - } - } From 4e7b3b9bdb6afa91ea3d8b71ad3296b6150b7890 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 19:11:32 +0100 Subject: [PATCH 16/21] Add unit tests for `CfDnsClient`, Fluent API, and `CfDnsClientBuilder`. Minor error message refinement in `CfBasicHttpClient`. --- .../codes/thischwa/cf/CfBasicHttpClient.java | 8 +- .../thischwa/cf/CfBasicHttpClientTest.java | 157 +++++++ .../java/codes/thischwa/cf/CfClientTest.java | 1 - .../thischwa/cf/CfDnsClientBuilderTest.java | 150 +++++++ .../thischwa/cf/CfDnsClientMockTest.java | 399 ++++++++++++++++++ .../thischwa/cf/fluent/FluentApiTest.java | 248 +++++++++++ 6 files changed, 958 insertions(+), 5 deletions(-) create mode 100644 src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java create mode 100644 src/test/java/codes/thischwa/cf/CfDnsClientBuilderTest.java create mode 100644 src/test/java/codes/thischwa/cf/CfDnsClientMockTest.java create mode 100644 src/test/java/codes/thischwa/cf/fluent/FluentApiTest.java diff --git a/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java b/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java index c51f6f5..6c8c6a4 100644 --- a/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java +++ b/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java @@ -33,6 +33,9 @@ abstract class CfBasicHttpClient { private final CfDnsClientBuilder.CfAuth auth; private final ObjectMapper objectMapper; + private record ResultWrapper(int statusCode, String responseBody) { + } + /** * Creates a new Cloudflare HTTP client with the specified base URL and authentication. * @@ -97,7 +100,7 @@ abstract class CfBasicHttpClient { 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); } } @@ -187,7 +190,4 @@ abstract class CfBasicHttpClient { private String buildUrl(String endpoint) { return baseUrl + endpoint; } - - private record ResultWrapper(int statusCode, String responseBody) { - } } diff --git a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java new file mode 100644 index 0000000..485491d --- /dev/null +++ b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java @@ -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 testGetRequest(String endpoint, Class responseType) + throws CloudflareApiException { + return getRequest(endpoint, responseType); + } + + public T testPostRequest(String endpoint, Object payload, Class responseType) + throws CloudflareApiException { + return postRequest(endpoint, payload, responseType); + } + + public T testPutRequest(String endpoint, Object payload, Class responseType) + throws CloudflareApiException { + return putRequest(endpoint, payload, responseType); + } + + public T testPatchRequest(String endpoint, Object payload, Class responseType) + throws CloudflareApiException { + return patchRequest(endpoint, payload, responseType); + } + + public T testDeleteRequest(String endpoint, Class 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("API error: 10404: No route for that URI")); + } + + @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); + }); + } +} diff --git a/src/test/java/codes/thischwa/cf/CfClientTest.java b/src/test/java/codes/thischwa/cf/CfClientTest.java index 75e06dd..dce178a 100644 --- a/src/test/java/codes/thischwa/cf/CfClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfClientTest.java @@ -198,7 +198,6 @@ public class CfClientTest { // test recordList with types without SLD List aList = client.recordList(z, RecordType.A); assertFalse(aList.isEmpty()); - assertTrue(aList.size() >= 1); assertTrue(aList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId()))); assertTrue(aList.stream().noneMatch(re -> re.getId().equals(createdRe2.getId()))); assertTrue(aList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType()))); diff --git a/src/test/java/codes/thischwa/cf/CfDnsClientBuilderTest.java b/src/test/java/codes/thischwa/cf/CfDnsClientBuilderTest.java new file mode 100644 index 0000000..aefeeab --- /dev/null +++ b/src/test/java/codes/thischwa/cf/CfDnsClientBuilderTest.java @@ -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); + } +} diff --git a/src/test/java/codes/thischwa/cf/CfDnsClientMockTest.java b/src/test/java/codes/thischwa/cf/CfDnsClientMockTest.java new file mode 100644 index 0000000..bc5b7f9 --- /dev/null +++ b/src/test/java/codes/thischwa/cf/CfDnsClientMockTest.java @@ -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 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 records = List.of(rec1, rec2, rec3); + Map> 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> 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 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 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 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 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 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 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()); + } +} diff --git a/src/test/java/codes/thischwa/cf/fluent/FluentApiTest.java b/src/test/java/codes/thischwa/cf/fluent/FluentApiTest.java new file mode 100644 index 0000000..f87c330 --- /dev/null +++ b/src/test/java/codes/thischwa/cf/fluent/FluentApiTest.java @@ -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 expectedRecords = List.of(rec1, rec2); + + when(mockClient.recordList(eq(testZone), any(RecordType[].class))) + .thenReturn(expectedRecords); + + ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone); + List 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 expectedRecords = List.of(rec1); + + when(mockClient.recordList(eq(testZone), any(RecordType[].class))) + .thenReturn(expectedRecords); + + ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone); + List 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 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 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 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 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); + } +} From 60005d7d6e6daab0916e7dfe1d7d422253e8e9c2 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 19:21:33 +0100 Subject: [PATCH 17/21] Comment out hello world example in `.woodpecker/maven.yml`. --- .woodpecker/maven.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/maven.yml b/.woodpecker/maven.yml index 6cd51bf..da10043 100644 --- a/.woodpecker/maven.yml +++ b/.woodpecker/maven.yml @@ -4,10 +4,10 @@ when: branch: develop steps: - - name: hello - image: alpine - commands: - - echo "Hello World!" +# - name: hello +# image: alpine +# commands: +# - echo "Hello World!" - name: maven verify image: maven:3-amazoncorretto-17-alpine From 0ce37c53aa32bd165af04c03c215c6f2e2b29469 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 19:35:17 +0100 Subject: [PATCH 18/21] Refine assertion error message in `CfBasicHttpClientTest`. --- src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java index 485491d..1d844b2 100644 --- a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java @@ -82,7 +82,7 @@ class CfBasicHttpClientTest { assertTrue(exception.getMessage().contains("Unexpected error")); Throwable cause = exception.getCause(); assertInstanceOf(CloudflareApiException.class, cause); - assertTrue(cause.getMessage().contains("API error: 10404: No route for that URI")); + assertTrue(cause.getMessage().contains("API error: 10404: No route for that URI"), "Expected error message No route for that URI"); } @Test From f180e7daba20bcb655c9db36f0f1bfd8731f9dd7 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 19:38:17 +0100 Subject: [PATCH 19/21] Refine assertion error message in `CfBasicHttpClientTest`. --- src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java index 1d844b2..0a27ec8 100644 --- a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java @@ -82,7 +82,7 @@ class CfBasicHttpClientTest { assertTrue(exception.getMessage().contains("Unexpected error")); Throwable cause = exception.getCause(); assertInstanceOf(CloudflareApiException.class, cause); - assertTrue(cause.getMessage().contains("API error: 10404: No route for that URI"), "Expected error message No route for that URI"); + assertTrue(cause.getMessage().contains("API error: 10404: No route for that URI"), "Expected error message: No route for that URI∞ but it was: " + cause.getMessage()); } @Test From e42579541a7e78a0023bfcb70c0e653f5f924270 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 19:41:16 +0100 Subject: [PATCH 20/21] Refine assertion error message in `CfBasicHttpClientTest`. --- src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java index 0a27ec8..0c0e6cd 100644 --- a/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfBasicHttpClientTest.java @@ -82,7 +82,7 @@ class CfBasicHttpClientTest { assertTrue(exception.getMessage().contains("Unexpected error")); Throwable cause = exception.getCause(); assertInstanceOf(CloudflareApiException.class, cause); - assertTrue(cause.getMessage().contains("API error: 10404: No route for that URI"), "Expected error message: No route for that URI∞ but it was: " + cause.getMessage()); + assertTrue(cause.getMessage().contains("No route for that URI"), "Expected error message: No route for that URI, but it was: " + cause.getMessage()); } @Test From ac1fd02b2fcca6565af02ddc45bcee3cee51c85e Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Tue, 10 Mar 2026 19:51:31 +0100 Subject: [PATCH 21/21] Update README: clarify test coverage improvements and refine wording in code quality notes --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e8f9ac8..fda986b 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ The dependency is: - 0.4.0-SNAPSHOT: - **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()`) @@ -64,7 +65,7 @@ The dependency is: - 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: eliminated duplication in batch operations, improved type safety in HTTP methods, + - 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