416 Commits
Author SHA1 Message Date
thischwa 9a312b464e [maven-release-plugin] prepare release v0.5.0
Build and Analyse / build-and-analyse (push) Successful in 25s
2026-09-07 09:25:59 +02:00
gitea-actions-bot bc373dad59 ci: update test report [skip ci] 2026-09-07 06:51:02 +00:00
thischwa b13a9ad02e install python3 in publish-report action
Build and Analyse / build-and-analyse (push) Successful in 24s
2026-09-07 08:50:29 +02:00
thischwa 65339f900d add java distribution input to setup-java-maven action
Build and Analyse / build-and-analyse (push) Failing after 43s
2026-09-07 08:29:39 +02:00
thischwa 7e065552a6 simplify action inputs by removing java distribution configuration
Build and Analyse / build-and-analyse (push) Failing after 5s
2026-09-07 08:21:12 +02:00
thischwa 6a11d881e1 add build and analyse workflow badge to README.md
Build and Analyse / build-and-analyse (push) Failing after 46s
2026-09-07 08:08:11 +02:00
gitea-actions-bot 7446dcc659 ci: update test report [skip ci] 2026-08-05 15:43:08 +00:00
thischwa 42e06245e1 fix #1 migrate codebase to Jackson 3.x 2026-08-05 17:42:04 +02:00
gitea-actions-bot 36d44ed7a4 ci: update test report [skip ci] 2026-08-05 15:04:39 +00:00
thischwa 68846b6676 update issue tracker link in pom.xml 2026-08-05 17:02:36 +02:00
gitea-actions-bot a651d478b0 ci: update test report [skip ci] 2026-07-21 17:35:05 +00:00
thischwa 3c480f19f8 add generation timestamp to JUnit test report 2026-07-21 19:33:51 +02:00
thischwa 32a35781b5 set Maven compiler source and target to java.version in pom.xml 2026-07-12 19:41:43 +02:00
thischwa 59111ea53c LICENSE aktualisiert 2026-07-12 18:19:55 +02:00
thischwa 982a95451d set Maven compiler source and target to java.version in pom.xml 2026-07-10 17:19:31 +02:00
thischwa be2fb5d89e update issue tracker link in README.md 2026-07-10 15:46:46 +02:00
thischwa d213f72138 update Maven package link in README.md 2026-07-08 19:34:07 +02:00
thischwa 824bcf060d add javadoc badge to README.md 2026-07-08 19:24:54 +02:00
thischwa dbedcdfe16 update javadoc link in README.md 2026-07-07 18:53:01 +02:00
thischwa 87b39fca7b update javadoc link in README.md 2026-07-03 18:16:30 +02:00
thischwa d052555ba7 Merge remote-tracking branch 'origin/develop' into develop 2026-06-28 19:38:24 +02:00
thischwa dfa2753619 - moved git actions to gitea actions
- removed sonarqube
- added report for junit tests
2026-06-28 19:37:50 +02:00
thischwa 0a47bb6338 remove javadoc 2026-06-28 19:09:53 +02:00
thischwa 25228af9f0 Generate Javadocs for BatchResponse and RecordType classes in the codes.thischwa.cf.model package. 2026-06-28 18:20:39 +02:00
thischwa d4fba87e13 Migrate GitHub Actions workflows and custom actions to Gitea, replacing .github directory with .gitea. 2026-06-28 13:09:05 +02:00
gitea-actions-bot 9919230a3b ci: update test report [skip ci] 2026-06-28 10:49:25 +00:00
thischwa ba9bc9ea93 Update publish-report action to write output to test.md instead of index.md 2026-06-28 12:48:34 +02:00
thischwa eea6e72145 Update changelog for version 0.5.0-SNAPSHOT: migrate to git.mein-gateway.de and replace SonarQube with custom actions 2026-06-27 19:39:27 +02:00
gitea-actions-bot c68b865798 ci: update test report [skip ci] 2026-06-27 17:03:10 +00:00
thischwa 41d35fe745 Add JaCoCo coverage parsing to publish-report action; enhance Markdown report with coverage details. 2026-06-27 19:02:19 +02:00
thischwa c2d10bb929 Clarify step names in publish-report action for improved readability. 2026-06-27 18:53:53 +02:00
thischwa f3e05e1bc7 Extract changelog to a dedicated changelog.md file and update references in README.md. 2026-06-27 18:42:12 +02:00
gitea-actions-bot 90664f4007 ci: update test report [skip ci] 2026-06-27 16:25:55 +00:00
thischwa 2c233437da Merge remote-tracking branch 'origin/develop' into develop 2026-06-27 18:25:14 +02:00
thischwa 84c5295345 Simplify publish-report action by removing unnecessary variable assignment in JUnit file processing. 2026-06-27 18:24:56 +02:00
thischwa 6d139329f1 reports/index.html gelöscht 2026-06-27 18:03:27 +02:00
gitea-actions-bot b5caee443e ci: update test report [skip ci] 2026-06-27 16:00:03 +00:00
thischwa 053979d4e1 Update publish-report to simplify inputs, improve Markdown generation, and adjust token usage for Gitea compatibility 2026-06-27 17:59:14 +02:00
thischwa d57da1e60f Switch publish-report action to generate Markdown test reports instead of HTML; modify report structure and formatting. 2026-06-27 17:51:48 +02:00
gitea-actions-bot 3613df6974 ci: update test report [skip ci] 2026-06-27 13:11:18 +00:00
thischwa 0acf0d0834 Refactor publish-report to merge JUnit files into single report for HTML generation; remove Maven cache step from setup-java-maven. 2026-06-27 15:10:19 +02:00
thischwa 5ed9da4036 Add HTML report generation and auto-commit to publish-report action 2026-06-27 14:58:30 +02:00
thischwa 84a37b8a8d Merge remote-tracking branch 'origin/develop' into develop 2026-06-27 14:17:13 +02:00
thischwa c292c27444 wip test report 2026-06-27 14:16:41 +02:00
thischwa 744abd47ff Remove SonarCloud integration from project and related configuration files. 2026-06-27 14:11:37 +02:00
thischwa 9f301337b6 Remove SonarCloud integration from project and related configuration files. 2026-06-25 19:18:21 +02:00
thischwa 0a401b164c Migrate project hosting and repository URLs from Codeberg to Gitea. 2026-06-25 18:55:00 +02:00
thischwa 7b08820d1a Simplify Maven repository setup instructions in README. 2026-04-23 12:58:36 +02:00
thischwa 83995ba5fa Update actions/cache to v5 in setup-java-maven workflow 2026-04-02 19:15:21 +02:00
thischwa d586567fd6 Merge remote-tracking branch 'origin/develop' into develop 2026-04-02 19:01:52 +02:00
thischwa 3505aee124 Update actions: bump setup-java to v5, checkout to v6, and test-reporter to v3 2026-04-02 19:01:33 +02:00
th-schwarz ee3dcda8ea .github/actions/publish-report/action.yml aktualisiert 2026-04-02 18:44:55 +02:00
thischwa c01946fc05 Update version to 0.5.0-SNAPSHOT and fix changelog entry formatting 2026-03-11 13:22:49 +01:00
thischwa 09cb00db19 [maven-release-plugin] prepare for next development iteration 2026-03-11 13:15:43 +01:00
thischwa a5cf641d27 [maven-release-plugin] prepare release v0.4.0 2026-03-11 13:15:36 +01:00
thischwa 2a76c9e469 Merge remote-tracking branch 'origin/develop' into develop 2026-03-11 11:08:50 +01:00
thischwa 8bbcebc53c Add GitHub Actions for Maven setup, JUnit reporting, and SonarCloud analysis. Update README with SonarCloud badges, embed assets, and document breaking changes. Refactor APIs with paging support, add model class tests, and improve error messages and code quality. Fix #13 and fix #15. 2026-03-11 11:08:04 +01:00
thischwa ac1fd02b2f Update README: clarify test coverage improvements and refine wording in code quality notes 2026-03-10 19:51:31 +01:00
thischwa e42579541a Refine assertion error message in CfBasicHttpClientTest. 2026-03-10 19:41:16 +01:00
thischwa f180e7daba Refine assertion error message in CfBasicHttpClientTest. 2026-03-10 19:38:17 +01:00
thischwa 0ce37c53aa Refine assertion error message in CfBasicHttpClientTest. 2026-03-10 19:35:17 +01:00
thischwa 60005d7d6e Comment out hello world example in .woodpecker/maven.yml. 2026-03-10 19:21:33 +01:00
thischwa 4e7b3b9bdb Add unit tests for CfDnsClient, Fluent API, and CfDnsClientBuilder. Minor error message refinement in CfBasicHttpClient. 2026-03-10 19:11:32 +01:00
thischwa 66a5d48927 reduce code smells 2026-03-10 15:10:52 +01:00
thischwa 6e71ba266a reduce code smells 2026-03-10 15:00:35 +01:00
thischwa be409139d7 issue #15 Add paging support in recordList API, update tests, and streamline query parameter names. 2026-03-10 14:50:34 +01:00
thischwa 8d2fc74d04 Add JUnit tests for model classes: RecordEntityTest, ZoneEntityTest, BatchEntryTest, RecordTypeTest, and update PagingRequestTest. 2026-03-10 09:58:58 +01:00
thischwa aaae19f783 Update README: document breaking change in client.zone().record() renaming to client.zone().getRecord() 2026-03-08 18:04:07 +01:00
thischwa 092412cf92 fixed docu 2026-03-08 17:53:56 +01:00
thischwa abb9704a87 close #13 2026-03-08 12:53:14 +01:00
thischwa 6a11868a0b Update docs: replace Codeberg image in assets 2026-03-08 12:04:50 +01:00
thischwa f016067931 Update README: add Codeberg image with link and embed asset in docs 2026-03-08 11:58:14 +01:00
thischwa c857c0d233 Update README: add SonarCloud badges for quality, security, coverage, LOC, and code smells 2026-03-08 11:43:38 +01:00
thischwa dd1052cd75 Update workflow: rename CF_API_TOKEN to API_TOKEN for consistency 2026-03-08 11:18:55 +01:00
thischwa 32618a942b Streamline SonarCloud scan step by using multiline syntax in GitHub Actions workflow. 2026-03-08 11:11:30 +01:00
thischwa 4d7deb3f2c Simplify Maven setup by removing unused settings configuration and improving readability of SonarCloud scan step. 2026-03-08 10:50:21 +01:00
thischwa 85f462e002 Add GitHub Actions for Maven setup, JUnit reporting, and build analysis with SonarCloud. 2026-03-08 10:41:45 +01:00
thischwa d1e2a78f24 Add GitHub Actions for Maven setup, JUnit reporting, and build analysis with SonarCloud. 2026-03-08 10:33:30 +01:00
thischwa 233c3988bc Merge remote-tracking branch 'origin/develop' into develop 2026-03-07 19:18:05 +01:00
thischwa b199495d55 Update README with Codeberg Issues link and pipeline-specific badge. Remove SonarCloud configuration from pom.xml and workflow. 2026-03-07 19:17:33 +01:00
thischwa 8b69b2551a Update README: replace GitLab Issues link with Codeberg Issues link. 2026-03-07 19:16:09 +01:00
thischwa eb2a96a482 Update README: replace Codeberg CI badge with pipeline-specific badge link. 2026-03-07 19:11:13 +01:00
thischwa b864e019c1 Remove SonarCloud configuration and scan step from pom.xml and Woodpecker workflow. 2026-03-07 18:51:34 +01:00
thischwa fc7952f8a2 Update SonarCloud configuration: specify develop as main branch in pom.xml and streamline workflow in first-test.yml. 2026-03-07 18:27:11 +01:00
thischwa 378c3e5d9b Simplify SonarCloud scan command in Woodpecker workflow in first-test.yml. 2026-03-07 18:08:38 +01:00
thischwa a6b76a00bf . 2026-03-07 17:44:35 +01:00
thischwa 9a525ace3b Remove test step from Woodpecker workflow in first-test.yml. 2026-03-07 17:32:18 +01:00
thischwa baf6c6a8a3 Update Woodpecker workflow: refine SonarCloud scan command in first-test.yml 2026-03-07 17:28:57 +01:00
thischwa 729799faef Enhance Woodpecker workflow: add sonarcloud scan and test steps, update Maven configuration in pom.xml. 2026-03-07 17:21:35 +01:00
thischwa d7d49d10fb Remove Javadoc generation and deployment step from first-test.yml. 2026-03-07 13:42:10 +01:00
thischwa 813cb3a65b Remove Javadoc generation and deployment step from first-test.yml. 2026-03-07 13:02:41 +01:00
thischwa 78310e5639 Combine Javadoc generation and deployment steps into a single task in first-test.yml. 2026-03-07 12:54:07 +01:00
thischwa 3b83e65e21 . 2026-03-07 12:48:18 +01:00
thischwa ce460ee4d8 Enhance Woodpecker workflow: add log message for Javadoc deployment in first-test.yml. 2026-03-07 12:39:32 +01:00
thischwa a3b1a542b2 Fix Woodpecker workflow: update secret key reference in first-test.yml. 2026-03-07 12:24:55 +01:00
thischwa 163a096f81 Fix Woodpecker workflow: correct environment variable declaration in first-test.yml. 2026-03-07 12:20:37 +01:00
thischwa a0016d7ca3 Enhance Woodpecker workflow: add Javadoc generation and deploy to pages branch 2026-03-07 12:17:56 +01:00
thischwa e1600d1b4e Simplify Woodpecker workflow: remove redundant checkout step 2026-03-07 11:27:40 +01:00
thischwa 116350b65d Enhance Woodpecker workflow: add checkout and Maven compile steps 2026-03-07 11:23:21 +01:00
thischwa 9d41ddc2e5 Update Woodpecker workflow: switch branch to develop and add test step 2026-03-07 11:07:12 +01:00
thischwa 53b10b142e Replace Maven CI pipeline with a simple "Hello World" workflow in .woodpecker. 2026-03-07 11:01:23 +01:00
thischwa 6abd0ce862 Simplify Maven CI workflow: remove unused steps and configurations in .woodpecker/maven.yml. 2026-03-07 10:39:29 +01:00
thischwa 579aa4971b Migrate Maven CI workflow from Forgejo Actions to Woodpecker 2026-03-07 10:34:25 +01:00
thischwa 725d8e5698 Add Forgejo Actions CI workflow for Maven build and artifact management 2026-03-07 10:08:02 +01:00
thischwa 2b359cbd88 Replace deprecated setSerializationInclusion with setDefaultPropertyInclusion in JsonConf. 2026-03-04 12:27:46 +01:00
thischwa 7b83b99cf1 Update dependency versions in pom.xml: Jackson to 2.21.1, Logback to 1.5.25 2026-03-04 11:35:14 +01:00
thischwa e90f4406e8 Remove Forgejo Actions workflow for Maven build and packaging 2026-03-03 18:29:21 +01:00
thischwa 34b3f98d37 Revert "Add forgejo Actions workflow for Maven build and packaging"
This reverts commit eb8eb1ca43.
2026-03-03 18:22:34 +01:00
thischwa eb8eb1ca43 Add forgejo Actions workflow for Maven build and packaging 2026-03-03 18:20:39 +01:00
thischwa 85b71b851d Add forgejo Actions workflow for Maven build and packaging 2026-03-03 18:15:57 +01:00
thischwa 2480d12a16 Merge remote-tracking branch 'origin/develop' into develop 2026-03-03 17:44:29 +01:00
thischwa 880a6c7380 Update pom.xml and README.md: Migrate repository URLs from GitLab to Codeberg. 2026-03-03 17:44:09 +01:00
th-schwarz 69bbda1493 .gitlab-ci.yml gelöscht 2026-03-03 14:11:45 +01:00
thischwa cd2bc207ef Update pom.xml: Migrate project settings from GitLab to Codeberg. 2026-03-03 14:09:40 +01:00
thischwa d83c364660 Update README.md: Finalize version in changelog from 0.3.0-SNAPSHOT to 0.3.0. 2026-02-15 17:35:22 +01:00
thischwa 2efcb24321 [maven-release-plugin] prepare for next development iteration 2026-02-15 17:33:39 +01:00
thischwa 62b86ca22f [maven-release-plugin] prepare release v0.3.0 2026-02-15 17:33:34 +01:00
thischwa f12f5d6db1 Update .gitlab-ci.yml: Adjust apidocs path, and enhance documentation in ResultInfo and CfDnsClientBuilder. 2026-01-25 18:59:02 +01:00
thischwa c435a80966 Update pom.xml: Bump JUnit to 5.14.2, Mockito to 5.21.0, and Maven Release Plugin to 3.3.1. 2026-01-25 18:13:03 +01:00
thischwa 1e8fe53891 Update pom.xml: Bump dependency and plugin versions (Jackson: 2.19.1, HttpClient: 5.5.1, JUnit: 5.12.3, Maven Javadoc Plugin: 3.11.2, Release Plugin: 3.1.2, Jacoco Plugin: 0.8.13). 2026-01-25 17:57:27 +01:00
thischwa 10ad4bd7f3 Update version in pom.xml from 0.2.1-SNAPSHOT to 0.3.0-SNAPSHOT. 2026-01-25 14:39:49 +01:00
thischwa 84a454f656 fixed changelog 2026-01-25 14:30:59 +01:00
thischwa 4a3ec42fa5 reducing code smells 2026-01-23 18:32:54 +01:00
thischwa 9e09b12dc3 Update README.md: Remove duplicate "de-lombok the source jar" entry in changelog. 2026-01-23 17:55:44 +01:00
thischwa d0802fc01d Update pom.xml: Bump Checkstyle to 12.3.0, configure maven-jar-plugin with version 3.4.2, and adjust Checkstyle properties. 2026-01-23 17:40:53 +01:00
thischwa 607b634b35 issue #11 - Refactor CfDnsClient with a simplified CfDnsClientBuilder for authentication and configuration. Update tests and README for new builder. Added authtification with apiToken 2026-01-23 17:26:58 +01:00
thischwa b593ca7311 issue #11 -Implement authentication refactor: Add CfAuth interface, ApiTokenAuth and EmailKeyAuth implementations, and CfAuthBuilder for flexible authentication in CfDnsClient. Update tests and adjust related classes for compatibility. 2026-01-23 15:36:17 +01:00
thischwa 773573bd39 Update README.md: release 0.2.0 details and note de-lomboked source jar. 2026-01-19 18:21:45 +01:00
thischwa b890ae9d17 [maven-release-plugin] prepare for next development iteration 2026-01-19 18:03:50 +01:00
thischwa c80cfc8b66 [maven-release-plugin] prepare release v0.2.0 2026-01-19 18:03:46 +01:00
thischwa b07c818127 Update version in pom.xml from 0.2.0-beta-SNAPSHOT to 0.2.0-SNAPSHOT. 2026-01-19 18:02:10 +01:00
thischwa 249dfd2de5 fix #10 - Add lombok-maven-plugin for delomboking, configure maven-jar-plugin to attach delomboked sources, and update pom.xml with necessary properties and dependencies. 2026-01-18 15:22:32 +01:00
thischwa b241d8cf09 issue #9 - Refactor CfClientTest, CfDnsClient, and CfBasicHttpClient: improve error handling, enable record filtering, refine empty result behavior, and update tests for clarity and robustness. 2026-01-11 18:28:22 +01:00
thischwa b88f4f78ba Fix JavaDoc formatting inconsistencies in CfBasicHttpClient, CfDnsClient, and ZoneOperations to improve readability and maintain standard alignment. 2026-01-11 11:46:13 +01:00
thischwa 9e8ee5bb4a Refactor tests: Replace assertTrue(size >= 1) with assertFalse(isEmpty) for clarity and correctness. 2026-01-09 15:25:33 +01:00
thischwa 34010a4e77 issue #9: Add groupRecordsByFqdn utility to CfDnsClient, set zoneId in DNS record methods, and enhance tests for record validation and grouping. 2026-01-07 17:52:48 +01:00
thischwa 0f1248d08b issue #9: Refactor recordGet to recordList for consistency across API methods, update related documentation, and revise impacted tests. 2026-01-06 20:35:50 +01:00
thischwa acf2a2fc3b issue #9: Add recordList method to CfDnsClient, update Zone-level fluent API, and implement tests for DNS record listing with optional type filters. 2026-01-06 20:14:05 +01:00
thischwa 483b79b372 Simplify README code example by removing unnecessary line breaks and improving method chaining clarity. 2026-01-06 15:27:01 +01:00
thischwa 5a6a17798b Simplify README examples by removing unnecessary line breaks, standardizing method chaining, and improving code readability for batch DNS operations and fluent API usage. 2026-01-06 15:20:12 +01:00
thischwa edf1752e81 Simplify README examples by removing unnecessary line breaks, standardizing method chaining, and improving code readability for batch DNS operations and fluent API usage. 2026-01-06 15:07:26 +01:00
thischwa 25fd480e69 Standardize and simplify README code examples by eliminating unnecessary line continuations, improving readability, and enhancing method chaining for DNS operations. 2026-01-06 14:35:25 +01:00
thischwa 5098e17172 Simplify README examples by combining line continuations, improving method chaining clarity, and standardizing formatting for batch DNS operations. 2026-01-06 13:50:30 +01:00
thischwa a221de4792 issue #9:
Change default behavior of `emptyResultThrowsException` to `false`, update `ResponseValidator` for improved handling of single and multiple results, and enhance test coverage and documentation.

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

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

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

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

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

Refactor API method names for consistency (`zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` → `recordList`, `sldInfo` → `recordGet`) and update related documentation and tests.
2026-01-06 13:19:19 +01:00
thischwa 1bfea09aa9 Refactor sldInfo to improve JavaDoc, split overloaded methods, and enhance exception handling. 2025-12-31 16:45:59 +01:00
thischwa 12c75914b1 Add test for handling unknown SLDs with sldInfo method. 2025-12-31 16:30:49 +01:00
thischwa dd586be9f4 fix formatting in PagingRequest#getPagingParams JavaDoc and reorder RecordEntity#getName method. 2025-12-31 13:38:32 +01:00
thischwa 77a584afb6 fix issue #8: Refactor sldInfo to support filtering by multiple record types. Adjust related methods, tests, and documentation accordingly. 2025-12-31 13:19:56 +01:00
thischwa 6027e66afe Update changelog: CfClient#sldInfo must return multiple RecordEntries 2025-12-30 15:49:41 +01:00
thischwa 351730dc79 Bump version to 0.2.0-beta-SNAPSHOT and update changelog 2025-12-30 15:46:33 +01:00
thischwa c75f9d74ca fix #7: Refactor sldInfo to return a list of records, update related methods and tests accordingly. Improve logging and exception handling for batch DNS operations. 2025-12-30 15:34:55 +01:00
thischwa 1abe888a65 [maven-release-plugin] prepare for next development iteration 2025-12-30 13:01:18 +01:00
thischwa 80670c2a90 [maven-release-plugin] prepare release v0.2.0-beta.2 2025-12-30 13:01:11 +01:00
thischwa ce1f766326 Add maven-source-plugin to generate and attach source JARs, update changelog 2025-12-30 12:58:07 +01:00
thischwa a965f21d6b Add batch DNS operations documentation and refine logging format in CfDnsClient. Extend tests for record creation and deletion. 2025-12-27 16:48:26 +01:00
thischwa a3dc89e2c5 Update changelog with details for v0.2.0-beta.1 changes 2025-12-26 10:15:47 +01:00
thischwa 9c36fa1988 [maven-release-plugin] prepare for next development iteration 2025-12-26 10:00:45 +01:00
thischwa 335c9a6c5e [maven-release-plugin] prepare release v0.2.0-beta.1 2025-12-26 10:00:40 +01:00
thischwa 9dfb5f4c75 Revert pom.xml version to 0.2.0-beta.1-SNAPSHOT. 2025-12-26 09:57:22 +01:00
thischwa e12eaff137 fixed test for put 2025-12-26 09:47:52 +01:00
thischwa 596006276e fixed test for put 2025-12-25 21:18:30 +01:00
thischwa 43ff58febc Update pom.xml version to 0.2.0-beta.2-SNAPSHOT. 2025-12-25 20:52:03 +01:00
thischwa f2f7f662a1 [maven-release-plugin] prepare for next development iteration 2025-12-25 20:48:03 +01:00
thischwa 9a376415b3 [maven-release-plugin] prepare release v0.2.0-beta.1 2025-12-25 20:47:57 +01:00
thischwa 0eaca55e95 issue #5 - Update pom.xml version to 0.2.0-beta.1-SNAPSHOT, enhance logging, and refine DNS batch record processing method. 2025-12-25 20:43:31 +01:00
thischwa deb6fcf445 Reformat pom.xml for improved readability and consistency. 2025-12-25 16:34:47 +01:00
thischwa e18f8d7017 Update SonarQube organization and project key in pom.xml. 2025-12-25 16:33:53 +01:00
thischwa 4df6757ee4 Update SonarQube project key and organization in GitLab CI; add dynamic project key to analysis command. 2025-12-25 16:26:28 +01:00
thischwa 3d026dab1d Use non-interactive mode (-B) for SonarQube analysis in GitLab CI pipeline. 2025-12-25 16:05:17 +01:00
thischwa c20bb6bb1c Fix GitLab CI SonarQube script by removing unnecessary -B flag. 2025-12-25 16:00:51 +01:00
thischwa a5e96f38f8 Revert "Simplify SonarQube analysis command in GitLab CI pipeline."
This reverts commit 7f56c6f453.
2025-12-25 15:40:33 +01:00
thischwa 4cb9b9929e Revert "Add SonarQube Maven plugin to pom.xml configuration."
This reverts commit 67df00c36e.
2025-12-25 15:40:33 +01:00
thischwa 67df00c36e Add SonarQube Maven plugin to pom.xml configuration. 2025-12-25 15:25:59 +01:00
thischwa 7f56c6f453 Simplify SonarQube analysis command in GitLab CI pipeline. 2025-12-25 15:10:45 +01:00
thischwa a5d7992391 Enhance code readability by improving comments, formatting, and aligning annotations across classes. 2025-12-25 14:43:54 +01:00
thischwa b4ce867efc issue #6 - ResponseResultInfo#Errors: wrong object structure
issue #5 - Add batch DNS record operations and refactor Cloudflare API client.
2025-12-25 14:27:18 +01:00
thischwa eb821a2218 [maven-release-plugin] prepare for next development iteration 2025-08-19 12:30:24 +02:00
thischwa 7c42b111fc [maven-release-plugin] prepare release v0.1.0 2025-08-19 12:30:20 +02:00
thischwa 56b26a85c4 Update formatting in pom.xml for improved readability. Adjust version to 0.1.0-SNAPSHOT. 2025-08-19 12:29:52 +02:00
thischwa 8b6a7ce802 [maven-release-plugin] prepare release v0.1.0 2025-08-19 12:24:56 +02:00
thischwa 678285705e Merge remote-tracking branch 'origin/develop' into develop 2025-08-19 11:47:45 +02:00
thischwa 121b591bf7 Remove redundant release stage from GitLab CI configuration. 2025-08-19 11:47:08 +02:00
thischwa 7803854301 Reorder GitLab CI steps for better configuration and clarity in the release stage 2025-08-19 11:44:22 +02:00
thischwa d58b2fc0a6 Reorder GitLab CI steps for better configuration and clarity in the release stage 2025-08-19 11:39:34 +02:00
thischwa 5ddc60f1c6 Set default branch name during CI pipeline using CI_COMMIT_REF_NAME or fallback to release 2025-08-19 11:25:47 +02:00
thischwa 957039edac Add missing git installation step in release stage of GitLab CI configuration 2025-08-19 11:02:47 +02:00
thischwa e817b97755 set version before release 2025-08-19 10:54:40 +02:00
thischwa d09301faf0 Remove tags trigger from GitLab Pages deployment in CI configuration 2025-08-19 10:47:33 +02:00
thischwa 2d8ba6a6f7 Update README.md to reflect placeholder version and changelog for release 2025-08-19 10:34:45 +02:00
thischwa 5002e8f129 Update GitLab CI to match pre-release version pattern in release job 2025-08-19 10:28:24 +02:00
thischwa 5383aa4c1c Automate Maven release process and version tagging in GitLab CI pipeline 2025-08-19 10:19:26 +02:00
thischwa 31c2910c93 fix json error 2025-08-19 10:12:18 +02:00
thischwa c4aebef5dc Add release stage to GitLab CI pipeline for automating project releases 2025-08-19 09:48:07 +02:00
thischwa 8246007d92 Add CfClientPenTest for penetration testing and enhance CfClientTest with environment checks 2025-08-18 20:12:40 +02:00
thischwa 997d0df060 Refactor CfClientTest to improve test clarity, ensure cleanup, and enhance validation logic 2025-08-18 19:16:40 +02:00
thischwa d9334ba10b Update Javadoc comment in RecordSingleResponse to reference RecordEntity 2025-08-18 18:32:33 +02:00
thischwa a2a76a60f2 Add tags to GitLab Pages deployment trigger in CI configuration 2025-07-02 11:38:41 +02:00
thischwa 10d63c43ca Update GitLab CI messages and correct Javadoc URL in README.md 2025-07-02 11:31:30 +02:00
thischwa 85391083a9 Merge remote-tracking branch 'origin/develop' into develop 2025-07-02 11:21:39 +02:00
thischwa 150fbe8b16 Update GitLab CI to rename deploy step and add Pages deployment configuration 2025-07-02 11:20:39 +02:00
thischwa 7240d7d04e Fix typo in GitLab CI artifact path and include test report files in artifacts 2025-07-02 11:19:51 +02:00
thischwa 48bd84c829 Merge remote-tracking branch 'origin/develop' into develop 2025-07-01 20:21:51 +02:00
thischwa fffe69441e Simplify apidocs copying step in GitLab CI 2025-07-01 20:21:19 +02:00
thischwa 6c32cf1c15 Fix typo in GitLab CI artifact path and include test report files in artifacts 2025-07-01 20:20:32 +02:00
thischwa cc03d97199 Fix typo in GitLab CI artifact path and include test report files in artifacts 2025-06-30 20:14:38 +02:00
thischwa 4a4f45341c Fix typo in GitLab CI artifact path and include test report files in artifacts 2025-06-30 20:14:01 +02:00
thischwa b347c713fb Add Maven Surefire plugin to export test summaries 2025-06-30 19:58:10 +02:00
thischwa 95fa741a1d Fix typo in Javadoc URL in README.md to correct namespace link 2025-06-30 19:38:50 +02:00
thischwa 29425678cf Update project references to use updated GitLab namespace 2025-06-30 19:29:56 +02:00
thischwa db4b47ef55 Add license badge to README.md 2025-06-28 13:31:22 +02:00
thischwa de24c9f1ab Fix incorrect Javadoc URL in README.md 2025-06-09 19:09:07 +02:00
thischwa dde9f84204 Update GitLab CI to include apidocs directory in Pages deployment 2025-06-09 18:52:42 +02:00
thischwa 89df5cd734 Fix GitLab CI to correctly copy contents of Javadocs to public directory 2025-06-09 18:48:25 +02:00
thischwa 5f344a61e3 Fix typo in GitLab CI artifact path for the public directory 2025-06-09 18:45:16 +02:00
thischwa 8a2b54c937 Update GitLab CI to use verbose flag when copying files for Pages deployment 2025-06-09 18:40:47 +02:00
thischwa 89a8719032 Update GitLab CI to use verbose flag when copying files for Pages deployment 2025-06-09 17:55:43 +02:00
thischwa e33f2f6534 Fix GitLab CI to correctly copy Javadocs to the public directory 2025-06-09 17:52:14 +02:00
thischwa 0d83a271ed Add ls -all to GitLab CI for debugging public directory contents 2025-06-09 17:43:06 +02:00
thischwa 7583536c8f Adjust GitLab CI to update Javadoc deployment path 2025-06-09 17:36:10 +02:00
thischwa 0864d36995 Refactor GitLab CI to consolidate Pages deployment into existing stages 2025-06-09 17:30:54 +02:00
thischwa e48315ac7c Fix GitLab CI copy command for deploying Javadocs 2025-06-09 17:20:07 +02:00
thischwa 949d6dfada Remove Javadoc profile and related GitLab CI configuration 2025-06-09 17:17:22 +02:00
thischwa 41071aca77 remove javadoc 2025-06-09 16:50:09 +02:00
thischwa 585211ca40 Update Javadocs 2025-06-09 12:18:45 +00:00
thischwa 0e4d68d719 Update example IP address in Javadoc and adjust GitLab CI rules 2025-06-09 14:12:35 +02:00
thischwa f90c9a57fd Update example IP address in CfDnsClient Javadoc 2025-06-09 14:02:06 +02:00
thischwa dd7faf41c0 Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 14:01:12 +02:00
thischwa 32ca1a53e8 Refactor GitLab CI stages and fix example IP address in CfDnsClient documentation 2025-06-09 14:01:02 +02:00
thischwa 204221b17c Update Javadocs [skip ci] 2025-06-09 11:49:05 +00:00
thischwa e8dac5a7e3 Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 13:36:16 +02:00
thischwa cf11e065f8 Refactor GitLab CI stages and fix example IP address in CfDnsClient documentation 2025-06-09 13:36:06 +02:00
thischwa aba98e310a Update Javadocs [skip ci] 2025-06-09 11:34:11 +00:00
thischwa 843d61d11a Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 13:29:51 +02:00
thischwa 3e8a8bf14b Refactor GitLab CI stages and fix example IP address in CfDnsClient documentation 2025-06-09 13:29:41 +02:00
thischwa 78885a3b4f Update Javadocs [skip ci] 2025-06-09 11:22:55 +00:00
thischwa 00bc33060c Refactor GitLab CI stages and fix example IP address in CfDnsClient documentation 2025-06-09 13:17:03 +02:00
thischwa df1893fdb9 Refactor GitLab CI stages and fix example IP address in CfDnsClient documentation 2025-06-09 13:13:25 +02:00
thischwa 7eed3154ec Update Javadocs [skip ci] 2025-06-09 10:55:56 +00:00
thischwa 555d365eb9 Refactor GitLab CI stages and fix example IP address in CfDnsClient documentation 2025-06-09 12:51:17 +02:00
thischwa 8c9ae0a7ff Fix Javadoc update commit message and adjust stage definition in GitLab CI pipeline 2025-06-09 12:31:05 +02:00
thischwa 8e9438ac2a Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 12:15:19 +02:00
thischwa a091734611 fixed idention 2025-06-09 12:15:09 +02:00
thischwa c25562da56 Update Javadocs 2025-06-09 10:12:03 +00:00
thischwa fd04d48a23 Enhance Javadoc update script in CI pipeline with improved logging for push actions 2025-06-09 12:07:55 +02:00
thischwa 2c567d70b5 just a test commit 2025-06-09 11:51:06 +02:00
thischwa 05e0967a9f Improve Javadoc update logic in CI pipeline for conditional commits 2025-06-09 11:41:55 +02:00
thischwa 55dc201e74 Update Javadocs [skip ci] 2025-06-09 09:24:32 +00:00
thischwa de4f3a8a2b Update git checkout process in deploy_pages stage for enhanced branch handling 2025-06-09 11:04:15 +02:00
thischwa 19a570b933 Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 10:52:21 +02:00
thischwa a6d561657f just a test commit 2025-06-09 10:52:08 +02:00
thischwa aaaa3eb887 Update Javadocs [skip ci] 2025-06-09 08:38:28 +00:00
thischwa 35840e900a Use personal access token for git push command in deploy_pages stage 2025-06-09 10:34:06 +02:00
thischwa f38e451c2d Add GITLAB_USERNAME and GITLAB_USEREMAIL variables to CI pipeline configuration 2025-06-08 14:24:25 +02:00
thischwa 02d1822255 Reorganize Git configuration commands in deploy_pages stage 2025-06-08 13:16:20 +02:00
thischwa 2da79626b2 Switch to apk package manager for installing git in deploy_pages stage 2025-06-08 13:03:17 +02:00
thischwa e4b9126569 Install git in deploy_pages stage to support Javadoc generation in CI pipeline 2025-06-08 12:52:58 +02:00
thischwa eb404d7aac fix typo 2025-06-08 12:44:09 +02:00
thischwa 0215601b63 Simplify Javadoc generation command in deploy_pages stage of CI pipeline 2025-06-08 12:37:56 +02:00
thischwa 188ca09f39 Add deploy_pages stage and automate Javadoc deployment in CI 2025-06-08 12:33:45 +02:00
thischwa 74f8a55264 Update Javadoc files with the latest generated content
Updated HTML documentation to reflect recent changes in the codebase. Simplified and standardized formatting for consistency across all Javadoc files.
2025-06-08 11:44:32 +02:00
thischwa 018c1be918 Correct issue management URL in pom.xml 2025-06-08 11:41:51 +02:00
thischwa ef77ce2781 Update README formatting and enhance Javadoc examples
Standardized README parameter formatting for consistency. Enhanced Javadoc in `CfDnsClient` with new example for creating subdomain records.
2025-06-08 11:35:03 +02:00
thischwa 4763abc84b Clean up README formatting and reorganize changelog entries 2025-06-05 13:04:46 +02:00
thischwa d7f8ff884a . 2025-05-01 17:51:44 +02:00
thischwa d191d891b3 . 2025-05-01 17:49:25 +02:00
thischwa 6639a6d53d . 2025-05-01 17:29:12 +02:00
thischwa cb2bfda88d Removed obsolete constructors at the exceptions and Update Javadoc files with latest generated content. 2025-04-26 18:56:41 +02:00
thischwa a5f892b86a Refactor ResultInfo to use Java record
Replaced the ResultInfo class with a Java record for conciseness and immutability. Adjusted related validation logic and method calls to align with the new record structure. Minor updates to Javadoc documentation were also included.
2025-04-26 18:40:38 +02:00
thischwa 3efbe1eaae Update response handling logic in ResponseValidator
Refined the condition for exception handling based on 'emptyResultThrowsException' to clarify when exceptions are triggered versus returning empty results. Updated Javadoc output files to reflect the latest documentation changes.
2025-04-26 17:46:36 +02:00
thischwa 25855078f8 Add unit tests for ResponseValidator with Mockito integration
Introduced `ResponseValidatorTest` to comprehensively test the `ResponseValidator` class. Enhanced the project dependencies by updating JUnit to 5.12.2 and integrating Mockito with `mockito-junit-jupiter` 5.17.0 for mocking support. Clarified Javadoc for handling `RecordMultipleResponse` cases in `ResponseValidator`.
2025-04-26 11:38:05 +02:00
thischwa 697906a669 Refactor response handling with ResponseValidator class
Introduced a new ResponseValidator class to encapsulate response validation logic, improving code readability and maintainability. Updated CfDnsClient to delegate response validation to this new class. Adjusted tests to align with the refactor and ensure proper exception handling.
2025-04-25 18:56:35 +02:00
thischwa e33318e989 Update Javadoc generation timestamps
Regenerated Javadoc files with updated timestamps for consistency and accuracy. No functional or content changes have been made to the documentation.
2025-04-23 17:57:47 +02:00
thischwa 2e7fa7586a Update README with API version details and method categorization 2025-04-23 17:26:26 +02:00
thischwa 57934b7f46 Merge remote-tracking branch 'origin/develop' into develop 2025-04-23 17:26:16 +02:00
thischwa 31c5ab4533 Update README with API version details and method categorization 2025-04-23 17:13:35 +02:00
thischwa 21d0d1a6cb Update CI pipeline to exclude tagged builds
Added an `except` rule to the build job to skip execution for tagged commits. This ensures the build job runs only for relevant non-tagged changes.
2025-04-23 17:12:36 +02:00
thischwa 192bb59b6f Update CI pipeline to exclude tagged builds
Added an `except` rule to the build job to skip execution for tagged commits. This ensures the build job runs only for relevant non-tagged changes.
2025-04-23 16:43:23 +02:00
thischwa cf533a15a1 Merge remote-tracking branch 'origin/develop' into develop 2025-04-23 16:41:03 +02:00
thischwa d9ec4f8c44 Regenerate Javadoc files for consistency
Refactor record creation method to improve clarity

Renamed `recordCreate` to `recordCreateSld` to better align with its function of creating records specifically for SLDs. Updated relevant tests, documentation, and method references to use the new naming for consistency and improved readability
2025-04-23 16:33:04 +02:00
thischwa 28ed41e28e Regenerate Javadoc files for consistency
The Javadoc files have been regenerated to reflect updates in metadata including timestamps and headers. This ensures the documentation remains consistent with the latest source code revisions.
2025-04-23 16:31:24 +02:00
thischwa b3be440320 Regenerate Javadoc files for consistency
The Javadoc files have been regenerated to reflect updates in metadata including timestamps and headers. This ensures the documentation remains consistent with the latest source code revisions.
2025-04-23 16:30:40 +02:00
thischwa 51a96d3583 Refactor record creation method to improve clarity
Renamed `recordCreate` to `recordCreateSld` to better align with its function of creating records specifically for SLDs. Updated relevant tests, documentation, and method references to use the new naming for consistency and improved readability.
2025-04-23 16:29:05 +02:00
thischwa 41b44b22c2 Update Javadoc files for version 0.2.0-SNAPSHOT
Updated auto-generated Javadoc files to reflect changes for version 0.2.0-SNAPSHOT. This includes metadata, timestamps, and minor structural adjustments across multiple HTML files.
2025-04-22 18:18:49 +02:00
thischwa 579c5e07d7 Update README with dependency version and changelog
Updated the Maven dependency version to 0.1.0-beta.3 in the README. Added a new changelog section detailing version updates for better tracking.
2025-04-22 18:06:04 +02:00
thischwa 3d302f7518 [maven-release-plugin] prepare for next development iteration 2025-04-22 17:42:36 +02:00
thischwa 5fdb327948 [maven-release-plugin] prepare release v0.1.0-beta.3 2025-04-22 17:42:32 +02:00
thischwa 06a454d4ee Set sonar.test.exclusions in pom.xml
Added configuration to exclude test files from Sonar analysis. This ensures that test-related code is not included in the quality inspection metrics.
2025-04-22 07:53:50 +02:00
thischwa 6606030090 Update SonarQube project key and simplify CI script
Updated the SonarQube project key in `pom.xml` to reflect the new naming convention. Simplified the CI script by removing redundant project key specification, ensuring consistency and maintainability.
2025-04-22 07:45:45 +02:00
thischwa 51a93e36b8 Add SonarCloud project configuration to pom.xml
Included project key, name, and branch for SonarCloud integration. This enables better tracking and analysis of code quality on the develop branch.
2025-04-22 07:36:47 +02:00
thischwa 7873b239e3 Update Sonar Maven plugin version in CI pipeline
Removed the hardcoded version of the Sonar Maven plugin to use the latest available version. This ensures compatibility with updates and reduces maintenance overhead.
2025-04-22 07:26:25 +02:00
thischwa 1ee3efa6c6 Update Sonar settings and SCM details in project config
Added `sonar.sourceEncoding` and updated Sonar Maven plugin to a specific version for improved compatibility. Adjusted SCM values to reflect the correct repository URLs under the `thischwa` namespace.
2025-04-22 07:16:03 +02:00
thischwa c3b4a721f3 Update SonarCloud integration to use Maven plugin
Replaced Sonar Scanner CLI with Maven Sonar plugin in the CI workflow for better integration and simplicity. Added SonarCloud configuration properties to `pom.xml` for centralized management. This streamlines the setup and ensures consistency across the project.
2025-04-22 07:01:42 +02:00
thischwa ead7dc7de2 Update SonarCloud project identifier in badges
Updated the SonarCloud project identifier in README.md to reflect the correct project name. This ensures the badges display accurate data for the current project configuration.
2025-04-21 20:16:22 +02:00
thischwa 3e2cc7b4d7 . 2025-04-21 20:10:56 +02:00
thischwa 1f21271a03 Update project details in GitLab CI configuration
Replaced outdated project directory, project key, and organization values with updated ones. This ensures proper integration with SonarQube and maintains accuracy in the pipeline configuration.
2025-04-21 19:40:19 +02:00
thischwa c7e534a4ee Update SonarQube configuration in GitLab CI pipeline
Removed the `sonar.sources` property as it is redundant in the current setup. SonarQube automatically detects source files, simplifying the configuration. This ensures the pipeline remains clean and easier to maintain.
2025-04-21 15:27:36 +02:00
thischwa f056ad2f47 Enable SonarQube analysis on main Java source directory.
Added the `-Dsonar.sources` property to specify the `src/main/java` directory for SonarQube analysis. This ensures the main Java source files are included in the scan, improving code quality assessment accuracy.
2025-04-21 15:19:59 +02:00
thischwa a755baef15 Remove redundant Sonar test exclusions from CI config.
The test exclusions were overly broad and unnecessary as the tests are handled properly without them. This change simplifies the configuration while maintaining the intended functionality.
2025-04-21 15:02:22 +02:00
thischwa 0f2bd08d54 . 2025-04-21 14:58:24 +02:00
thischwa f9278fd4b7 Update SonarQube configuration in CI pipeline
Added the `sonar.tests` property to specify the test directory for SonarQube analysis. This ensures better accuracy in test coverage measurement and aligns with the project's structure.
2025-04-21 14:42:58 +02:00
thischwa aa1691c1e1 Refactor test formatting and add validation in CfClientTest
Adjusted inconsistent indentation and formatting in test cases for improved readability. Added additional test assertions for invalid input validation in the CfDnsClient constructor to enhance test coverage.
2025-04-21 14:27:45 +02:00
thischwa d2f0cc078d Merge remote-tracking branch 'origin/develop' into develop 2025-04-21 14:25:38 +02:00
thischwa 24021972a7 update javadoc 2025-04-21 14:21:27 +02:00
thischwa ed183bd393 update javadoc 2025-04-21 14:20:42 +02:00
thischwa 8fbfa43a1e update javadoc 2025-04-21 14:20:08 +02:00
thischwa e0f9043389 Refactor constants and add private constructor in JsonConf.
Changed API_EMAIL and API_KEY to static final for proper constant usage. Added a private constructor in JsonConf to prevent instantiation, ensuring adherence to utility class design principles.
2025-04-21 14:16:24 +02:00
thischwa c1675d0fba Refactor response handling and centralize JSON configuration
Replaced direct fields in `AbstractResponse` with `ResponseResultInfo` for better encapsulation and consistency. Introduced `JsonConf` to provide a shared, reusable `ObjectMapper` configuration. Updated tests and logic to align with the refactored response structure, enhancing maintainability and readability.
2025-04-21 14:06:25 +02:00
thischwa af4e09f938 Refactor code documentation and improve UI accessibility
Updated JavaDocs for consistency and clarity, modified styling with CSS variables for better maintainability, and enhanced script functionality for interactivity. Minor localization changes and accessibility improvements made to the generated documentation.
2025-04-19 11:34:22 +02:00
thischwa 4902d0062a Improve logging and update dependencies
Enabled detailed logging for DNS record operations and adjusted log levels. Updated Maven dependencies, including httpclient5, logback, and added explicit SLF4J configurations. Minor changes to the logging configuration file and project structure for better test isolation.
2025-04-18 13:19:40 +02:00
thischwa 0e5b878e9f Refactor DNS record handling and add record creation method
Introduced `buildFqdn` utility to avoid redundancy in FQDN construction. Added `recordCreate` method for simpler record creation with specified parameters. Enhanced `recordDeleteTypeIfExists` to handle multiple record types and improved test cases for better clarity and consistency.
2025-04-18 11:49:24 +02:00
thischwa bebe20a12d wip 2025-04-16 17:06:00 +02:00
thischwa 1815f94683 wip 2025-04-16 16:39:40 +02:00
thischwa 438916bd81 Merge remote-tracking branch 'origin/develop' into develop 2025-04-16 08:06:21 +02:00
thischwa 8f574b63eb Update SonarCloud badge project identifiers in README
Changed the project identifiers in SonarCloud badge URLs to align with the updated project naming convention. This ensures accurate integration and displays correct data on the README. No functionality or content outside of badges was affected.
2025-04-16 08:05:58 +02:00
thischwa 807a080636 test commit 2025-04-16 07:52:36 +02:00
thischwa eccb20bec3 wip 2025-04-15 20:15:47 +02:00
thischwa 23d4204157 wip 2025-04-15 20:10:37 +02:00
thischwa 9e165ee3b5 wip 2025-04-15 19:56:11 +02:00
thischwa 25b69c35e9 wip 2025-04-15 19:47:55 +02:00
thischwa 2b25c30fcd wip 2025-04-15 19:42:13 +02:00
admin 8895d4c82e Edit README.md 2025-04-15 17:24:06 +00:00
thischwa a24c262216 Merge remote-tracking branch 'origin/develop' into develop 2025-04-15 19:14:33 +02:00
thischwa 63284b19bc Update GitLab CI variables for project relocation
The GitLab CI configuration variables have been updated to reflect changes in the project location. The updates include adjustments to the clone directory, project key, and organization, ensuring that Sonar analysis and GitLab CI operations are correctly configured for the relocated project.
2025-04-15 19:14:12 +02:00
thischwa f7035fad01 Set Sonar branch name to 'develop' in CI pipeline
Added the Sonar branch name parameter to ensure proper tracking of the 'develop' branch during analysis. This change enhances consistency and accuracy in SonarQube reporting for this branch.
2025-04-15 19:13:28 +02:00
thischwa fe2f9c512f Set Sonar branch name to 'develop' in CI pipeline
Added the Sonar branch name parameter to ensure proper tracking of the 'develop' branch during analysis. This change enhances consistency and accuracy in SonarQube reporting for this branch.
2025-04-15 08:03:36 +02:00
thischwa 65b5ebcdfb update docu because of obsolete api-token 2025-04-14 10:14:57 +02:00
thischwa 16a586c080 Refactor code for consistent formatting and style. 2025-04-14 09:50:35 +02:00
thischwa 97b7f8371a Refactor code style and add tests for exception handling
Adjusted code style to align with consistent brace formatting and removed the null/blank check for `baseUrl` in the constructor. Added unit tests to verify exception handling in `CfDnsClient` for null arguments.
2025-04-13 19:57:15 +02:00
thischwa f0bc7b5446 Update README to reflect cloudflaredns version bump
Updated the Maven dependency version from 0.1.0-beta.1 to 0.1.0-beta.2. This ensures the documentation reflects the latest release for accurate usage.
2025-04-13 19:35:42 +02:00
thischwa 1bd0a6f963 [maven-release-plugin] prepare for next development iteration 2025-04-13 19:34:02 +02:00
thischwa 83ad5d7fe8 [maven-release-plugin] prepare release v0.1.0-beta.2 2025-04-13 19:33:58 +02:00
thischwa 9e3e12a08b Refactor constructor to explicitly throw IllegalArgumentException
Updated the constructor in CfBasicHttpClient to include explicit throws for IllegalArgumentException. This enhances clarity and ensures the method signature reflects the potential for exceptions. Minor formatting adjustments were also made for improved code readability.
2025-04-13 19:27:21 +02:00
thischwa 34ea8e1b62 fixed issue #3 2025-04-13 17:33:50 +02:00
thischwa f5ba156c49 Update Javadoc link and remove outdated documentation
Replaced the Javadoc link in the README with the updated URL and deleted the obsolete `docs/README.MD` file. This ensures that the documentation points to the correct and current resources.
2025-04-13 09:51:08 +02:00
thischwa 99939ed591 Add GitLab Pages deployment to CI configuration
This update introduces a new job to deploy GitLab Pages. The job copies content from the `docs/` directory to the `public/` directory and is triggered only on the `develop` branch.
2025-04-13 09:42:28 +02:00
thischwa 49a1d51ce8 Update version format in README dependency example
Corrected the version format to match standard semantic versioning. This ensures consistency and avoids potential confusion for users copying the dependency snippet.
2025-04-12 20:22:03 +02:00
thischwa 694965d6ec Update version format in README dependency example
Corrected the version format to match standard semantic versioning. This ensures consistency and avoids potential confusion for users copying the dependency snippet.
2025-04-12 19:35:47 +02:00
thischwa c4e5a3e6e5 [maven-release-plugin] prepare for next development iteration 2025-04-12 19:29:51 +02:00
thischwa 8fe07af331 [maven-release-plugin] prepare release v0.1.0-beta.1 2025-04-12 19:29:47 +02:00
thischwa a87bc48d3d wip 2025-04-12 19:27:37 +02:00
thischwa 07b2f3fd6d Edit pom.xml 2025-04-12 19:22:02 +02:00
thischwa 4172700ec4 [maven-release-plugin] prepare release v0.1.0-beta.1 2025-04-12 18:57:17 +02:00
thischwa fa09ae5987 Update repository URL and dependency version in README
Replaced the Maven repository URL with the new GitLab package URL and updated the dependency version from `0.1.0-beta.1` to `0.1.0.beta-2`. This ensures compatibility with the latest hosting and artifact version.
2025-04-12 18:36:00 +02:00
thischwa afbc7d5807 test commit 2025-04-12 18:25:58 +02:00
thischwa 2bf1a4fff7 Remove GitLab repository cloning workflow
This workflow is no longer needed as the repository cloning task has been deprecated or moved. Removing it helps streamline the GitHub Actions setup and eliminates unnecessary maintenance overhead.
2025-04-12 12:03:43 +02:00
thischwa a6c6e77a4b Replace build workflow with daily GitLab repository clone.
Removed the build-and-test.yml GitHub Actions workflow, including Maven build and Sonar integration. Introduced a new workflow to clone a GitLab repository daily using a scheduled cron job.
2025-04-11 19:31:27 +02:00
thischwa 448be91197 [maven-release-plugin] prepare for next development iteration 2025-04-10 18:45:37 +02:00
thischwa 9f0493744f [maven-release-plugin] prepare release v0.1.0.beta-2 2025-04-10 18:45:33 +02:00
thischwa b69799851c fix repo settings 2025-04-10 18:15:31 +02:00
thischwa 4aed622d14 wip 2025-04-09 18:29:18 +02:00
thischwa 49455a25d5 wip 2025-04-09 18:25:55 +02:00
thischwa e7ca5b90f7 wip 2025-04-09 18:23:27 +02:00
thischwa 7d3e944f48 wip 2025-04-09 18:18:46 +02:00
thischwa ead5bf04d5 wip 2025-04-09 17:42:47 +02:00
thischwa 5d85f14752 wip 2025-04-09 17:31:41 +02:00
thischwa da99731b57 wip 2025-04-09 17:30:06 +02:00
thischwa 924b265d91 Merge remote-tracking branch 'origin/develop' into develop 2025-04-08 19:38:45 +02:00
thischwa 5d950162ef wip 2025-04-08 19:38:25 +02:00
thischwa d553381abf Edit pom.xml 2025-04-08 19:18:56 +02:00
thischwa 9d16fca2d0 wip 2025-04-08 19:08:25 +02:00
thischwa 56080236ef Remove unused SonarQube configs and cache setup.
Cleaned up commented-out SonarQube configuration from `pom.xml` and removed redundant cache settings in `.gitlab-ci.yml`. This simplifies the build configuration and eliminates unused or unnecessary definitions.
2025-04-08 18:43:28 +02:00
thischwa 8fdb7887cf Update issue tracker URL and adjust SonarQube configurations
Switched issue tracking from GitHub to GitLab in `pom.xml`. Commented out SonarQube configurations in `pom.xml` and adjusted exclusions in `.gitlab-ci.yml` script. These changes align with the project's migration to GitLab and streamlined quality analysis setup.
2025-04-08 18:24:41 +02:00
thischwa 5ae9432958 Update sonar exclusions path to include all subdirectories
Refined the `sonar.exclusions` configuration in `pom.xml` to account for all subdirectories under `docs`. This ensures proper exclusion of nested files during SonarCloud analysis.
2025-04-08 17:56:30 +02:00
thischwa 21b188b913 Update Sonar exclusions path in pom.xml
Changed the Sonar exclusions path from `src/docs/**/*` to `docs/**` for better alignment with the project's folder structure. This ensures accurate exclusion of intended files during analysis.
2025-04-08 17:32:46 +02:00
thischwa 29352da798 Update SonarCloud badge URLs with authentication tokens
Updated the project identifiers and added tokens to SonarCloud badge links in the README for improved integration. These changes ensure accurate data retrieval and display on the project status badges.
2025-04-08 17:26:50 +02:00
thischwa 34bd08ba7b Add sonar exclusions for documentation files
Excluded `src/docs/**/*` from SonarCloud analysis to avoid unnecessary scans of documentation files. This helps focus the analysis on relevant source code and improves overall build clarity.
2025-04-07 10:24:04 +02:00
thischwa 0cab3af773 Edit .gitlab-ci.yml 2025-04-07 09:50:59 +02:00
thischwa 6dcb347d6d wip 2025-04-06 19:46:23 +02:00
thischwa e9d64e194c wip 2025-04-06 19:32:45 +02:00
thischwa cb36f3d1ac wip 2025-04-06 19:17:01 +02:00
thischwa 0ff97fece9 Add 'build' stage to GitLab CI pipeline
This change introduces a new 'build' stage to the CI pipeline. It sets up the pipeline for improved clarity and better stage separation, paving the way for future enhancements or steps.
2025-04-06 19:11:47 +02:00
thischwa 131ee9970a Disable pull stage and refactor build process in CI config.
The pull stage is commented out to streamline the CI pipeline. Refactored the build job to generate artifacts while ensuring test tasks depend on the output of the build job. Added dependencies to relevant stages for smoother execution flow.
2025-04-06 19:02:28 +02:00
thischwa 7810cb7d8e Update CI pipeline to include build step before Sonar scan
Added a Maven build step (`mvn clean package -DskipTests`) to ensure compiled binaries are available for the SonarQube analysis. Also specified the `sonar.java.binaries` parameter to point to the correct output directory.
2025-04-06 18:48:03 +02:00
thischwa 4d226716ec Update CI caching to use .maven-cache
Introduced a consistent cache path for Maven builds by replacing .sonar/cache with .maven-cache. This change optimizes build performance by ensuring proper caching of Maven dependencies across different jobs.
2025-04-06 18:31:58 +02:00
thischwa bf12bceb56 Update SonarQube scan configuration in CI pipeline
Added project key, organization, and token parameters to the `sonar-scanner` command for better integration and customization. This ensures proper identification and authentication with SonarQube during code analysis.
2025-04-06 18:23:40 +02:00
thischwa 9e8cd3b6c3 Optimize GitLab CI configuration for SonarCloud integration
Added support for caching SonarCloud analysis tasks and updated the Git depth setting to ensure proper branch
2025-04-06 18:09:56 +02:00
thischwa 7880ae781f Fix multiline formatting for SonarCloud scan script
Updated the GitLab CI configuration to properly format the SonarCloud scan script using a multiline approach. This ensures better readability and maintainability of the pipeline configuration.
2025-04-06 12:58:48 +02:00
thischwa ebf2be3505 Update CI image to Amazon Corretto 17 Alpine version
Switch the base image in the GitLab CI from Bullseye to Alpine. This change reduces image size and improves build efficiency. All pipeline stages remain unaffected.
2025-04-06 12:52:32 +02:00
thischwa f85e177c98 Update CI image to Amazon Corretto 17 Alpine version
Switch the base image in the GitLab CI from Bullseye to Alpine. This change reduces image size and improves build efficiency. All pipeline stages remain unaffected.
2025-04-06 11:58:58 +02:00
thischwa 91133d5f12 Use slimmer base image in GitLab CI configuration
Switch the CI pipeline base image to `maven:3-amazoncorretto-17-slim-bullseye` for a lighter and more efficient build environment. This change reduces unnecessary overhead while maintaining compatibility.
2025-04-06 11:53:49 +02:00
thischwa dcc3080686 Update CI image to maven:3-amazoncorretto-17
Switching to the new base image ensures compatibility with the latest repository updates. This change may improve build consistency and align with recommended CI configurations.
2025-04-05 21:05:50 +02:00
thischwa 4577ff72a7 Update CI image to maven:3-amazoncorretto-17
Switching to the new base image ensures compatibility with the latest repository updates. This change may improve build consistency and align with recommended CI configurations.
2025-04-05 21:02:40 +02:00
thischwa 59ecffc86a Add SonarCloud integration and enhance CI stages
Introduced SonarCloud scan and testing stages to the CI pipeline. Defined variables for SonarCloud and added a new `on_commit` stage for running tests. Updated the `.gitlab-ci.yml` configuration to improve project quality checks and automation.
2025-04-05 20:46:43 +02:00
thischwa 39b72e5ba8 Update CI pipeline to adjust clone job and add pull stage
Reorganized the cloning logic into a dedicated pull stage and updated the cloning script for efficiency. Replaced `rules` with `only` to clarify job triggers and added the `GITLAB_CLONE_DIR` variable for better repository management.
2025-04-01 18:11:40 +02:00
thischwa e0bbc62183 Update .gitlab-ci.yml file 2025-04-01 15:47:43 +00:00
thischwa 84593da57e Add GitLab CI pipeline to sync GitHub repository daily
Introduce a `.gitlab-ci.yml` file with a job to clone a GitHub repository using a personal access token. The pipeline is scheduled to run daily via a cron rule, ensuring the repository stays in sync.
2025-04-01 17:39:28 +02:00
thischwa 0c367217a6 Update README for clarity and improve wording consistency
Made minor changes to enhance readability and consistency in phrasing within the README file. Adjusted descriptions to make them more concise and user-friendly.
2025-03-30 17:38:47 +02:00
Thilo Schwarz 4eb923ebee Update README.md
Signed-off-by: Thilo Schwarz <th-schwarz@users.noreply.github.com>
2025-03-30 12:56:15 +02:00
Thilo Schwarz efd7f2228d Update README.md
Signed-off-by: Thilo Schwarz <th-schwarz@users.noreply.github.com>
2025-03-30 12:54:40 +02:00
thischwa fd997bf6da Add Maven repository and usage instructions to README
Updated the README to include instructions for integrating the project into a Maven-based setup, like adding the repository and dependency configurations. These changes assist developers in easily utilizing the library.
2025-03-30 12:51:56 +02:00
thischwa 1a1794ff37 Refactor Maven pom.xml for SCM and distribution setup.
Reorganized the `scm` block and added a `distributionManagement` section for publishing to GitHub Maven Packages. Cleaned up unused plugin configurations and improved XML formatting for better readability.
2025-03-27 19:08:26 +01:00
thischwa 0bc1cef900 Fix README badge formatting for GitHub release link
Updated the README to correctly format the GitHub release badge and its hyperlink. This enhances clarity and ensures the badge integrates seamlessly with the text.
2025-03-24 19:06:28 +01:00
thischwa 2c9faf882e [maven-release-plugin] prepare for next development iteration 2025-03-24 18:47:49 +01:00
thischwa 27b7048f97 [maven-release-plugin] prepare release v0.1.0-beta.1 2025-03-24 18:47:46 +01:00
thischwa 9dd7489803 Update version to 0.1.0-SNAPSHOT in pom.xml
Reverted the version from 0.1.1-SNAPSHOT to 0.1.0-SNAPSHOT for alignment with the development lifecycle. Ensures consistency with planned versioning strategy.
2025-03-24 18:42:25 +01:00
thischwa 006fb783e5 [maven-release-plugin] prepare for next development iteration 2025-03-24 18:26:15 +01:00
thischwa 9b5d791f9e [maven-release-plugin] prepare release v0.1.0 2025-03-24 18:26:11 +01:00
thischwa be7f1798bf Refactor javadoc plugin into a Maven profile.
Moved javadoc configuration and execution into a dedicated profile to improve build configuration modularity. This change ensures the javadoc generation is optional and can be controlled via the `javadoc` profile activation.
2025-03-24 18:24:22 +01:00
thischwa 4f835a22ef Add skipTests argument to Maven release plugin
This update configures the Maven release plugin to include the `-DskipTests` argument. It ensures tests are skipped during release processes, speeding up build times for releases.
2025-03-24 18:10:40 +01:00
thischwa 08a5606da6 reduce code smells 2025-03-24 15:49:43 +01:00
thischwa d8726e5e64 Add SonarCloud badges to README
Added badges for quality gate status, security rating, coverage, lines of code, and code smells from SonarCloud. These provide better visibility into the project's code quality and metrics directly from the README.
2025-03-24 15:44:03 +01:00
thischwa e77b392ffd Update workflow to rename job and fix Sonar project key
Renamed the `mvn-test` job to `mvn-sonar` for better clarity and alignment with its purpose. Additionally, corrected a typo in the Sonar project key to ensure accurate project identification.
2025-03-24 15:23:47 +01:00
thischwa ae14436b08 Simplify and merge Maven build and SonarCloud scan steps
Consolidated the Maven build and SonarCloud analysis into a single step for efficiency. Removed redundant workflow definitions and ensured proper environment variable handling. This change reduces complexity and streamlines the CI/CD workflow.
2025-03-24 15:07:12 +01:00
thischwa 5c1e7fe86d Add SonarCloud scan to CI workflow
Introduce a new job in the GitHub Actions workflow to integrate SonarCloud analysis. This includes setting up JDK 17, caching Maven dependencies, and running the SonarCloud Maven plugin for code quality checks.
2025-03-24 14:51:32 +01:00
thischwa efac34e73c Add SonarCloud and JaCoCo integration to pom.xml
Configured SonarCloud properties for code analysis and reporting. Integrated JaCoCo Maven plugin to generate code coverage reports in XML format for SonarCloud. These changes enhance code quality metrics and monitoring.
2025-03-24 10:07:21 +01:00
thischwa 3cd29e4e91 Add release badge to README 2025-03-24 10:00:54 +01:00
thischwa da661e7cd0 Add release badge to README
Included a GitHub release badge to display the latest release version directly in the README. This improves visibility of the project's current state and provides quick access to the release details.
2025-03-23 20:06:07 +01:00
thischwa fa5840999d Update Javadoc and example code for CfDnsClient
Regenerated all Javadoc files with updated timestamps and fixed an example snippet in `CfDnsClient` for consistent variable naming. This ensures updated documentation and code clarity.
2025-03-23 19:04:50 +01:00
thischwa 513d89f017 Update .gitignore and add jQuery UI overrides CSS
Added an entry for a javadoc script path in .gitignore to exclude it from version control. Also, introduced a custom CSS file to override specific styles in jQuery UI, primarily adjusting active state colors and borders.
2025-03-23 18:44:26 +01:00
thischwa e415dc1106 Refactor HTTP request methods and update Javadoc language.
Refactored HTTP request methods by removing explicit `responseType` parameters where possible and adjusting to specific response mapping needs. Additionally, updated Javadoc documentation language from German to English for consistency, improving clarity and accessibility.
2025-03-23 18:39:36 +01:00
thischwa af757714f4 Update README with corrected javadoc link
Replaced the broken relative javadoc link with a working absolute URL pointing to the hosted documentation. This ensures easier access to the API reference for users.
2025-03-23 13:18:08 +01:00
thischwa 6e05348f6a Remove obsolete javadoc.sh script
The javadoc.sh script was specific to a local environment and is no longer needed. Removing it ensures cleaner code and avoids confusion or potential misuse.
2025-03-23 13:13:49 +01:00
thischwa 30ffb7c4b4 Add Javadoc generation scripts and enhance README examples
Introduce Javadoc-related files (`javadoc.sh`, `options`, and `packages`) for API documentation generation. Update README to improve clarity on usage examples and remove redundant "Example" headings for a cleaner presentation.
2025-03-23 13:12:45 +01:00
thischwa 042678a6d7 Add usage examples for CfDnsClient in API documentation
Expanded the API documentation by including practical examples for the `CfDnsClient` class. Updated Javadoc metadata and modified the index.html title for better context.
2025-03-23 13:11:27 +01:00
thischwa a443c0feae Add initial documentation homepage 2025-03-23 12:36:14 +01:00
thischwa c39e022e41 Initial commit 2025-03-23 12:12:53 +01:00
58 changed files with 5859 additions and 14 deletions
+128
View File
@@ -0,0 +1,128 @@
name: Publish JUnit Report (Short Names)
description: Normalize JUnit XML report names and publish a test report to the repo.
inputs:
token:
description: Gitea token with write access to the repository.
required: true
runs:
using: composite
steps:
- name: Install python3
shell: bash
run: apk add --no-cache python3
- name: Normalize JUnit report names
shell: bash
run: |
mkdir -p junit-short
shopt -s globstar nullglob
for f in **/target/*-reports/TEST-*.xml; do
base="$(basename "$f")"
short="${base#TEST-}"
cp "$f" "junit-short/${short}"
done
- name: Generate Markdown Test Report
shell: bash
run: |
mkdir -p reports
python3 - <<'EOF'
import glob, xml.etree.ElementTree as ET
from datetime import datetime, timezone
generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
# --- JUnit ---
files = glob.glob("junit-short/*.xml")
total_tests = total_failures = total_errors = total_skipped = 0
test_rows = []
for f in sorted(files):
tree = ET.parse(f)
r = tree.getroot()
suites = r.findall("testsuite") if r.tag == "testsuites" else [r]
for ts in suites:
name = ts.get("name", f)
tests = int(ts.get("tests", 0))
failures = int(ts.get("failures", 0))
errors = int(ts.get("errors", 0))
skipped = int(ts.get("skipped", 0))
passed = tests - failures - errors - skipped
status = "✅" if (failures + errors) == 0 else "❌"
total_tests += tests
total_failures += failures
total_errors += errors
total_skipped += skipped
test_rows.append(f"| {status} | {name} | {tests} | {passed} | {failures + errors} | {skipped} |")
total_passed = total_tests - total_failures - total_errors - total_skipped
overall = "✅ All tests passed" if (total_failures + total_errors) == 0 else "❌ Some tests failed"
# --- JaCoCo ---
def counter(el, type_):
c = next((x for x in el.findall("counter") if x.get("type") == type_), None)
if c is None:
return 0, 0
covered = int(c.get("covered", 0))
missed = int(c.get("missed", 0))
return covered, covered + missed
cov_rows = []
jacoco_files = glob.glob("**/target/site/jacoco/jacoco.xml", recursive=True)
for jf in sorted(jacoco_files):
tree = ET.parse(jf)
root = tree.getroot()
for pkg in root.findall("package"):
name = pkg.get("name", "").replace("/", ".")
line_cov, line_total = counter(pkg, "LINE")
branch_cov, branch_total = counter(pkg, "BRANCH")
line_pct = f"{100 * line_cov / line_total:.0f}%" if line_total else "n/a"
branch_pct = f"{100 * branch_cov / branch_total:.0f}%" if branch_total else "n/a"
cov_rows.append(f"| {name} | {line_pct} ({line_cov}/{line_total}) | {branch_pct} ({branch_cov}/{branch_total}) |")
# --- Markdown zusammenbauen ---
md = (
"# Test Report\n\n"
f"_Generated on {generated_at}_\n\n"
f"**{overall}**\n\n"
"## Test Results\n\n"
"| | Tests | Passed | Failed | Skipped |\n"
"|---|---|---|---|---|\n"
f"| **Total** | {total_tests} | {total_passed} | {total_failures + total_errors} | {total_skipped} |\n\n"
"### Details\n\n"
"| Status | Suite | Tests | Passed | Failed | Skipped |\n"
"|---|---|---|---|---|---|\n"
) + "\n".join(test_rows) + "\n\n"
if cov_rows:
md += (
"## Coverage\n\n"
"| Package | Line Coverage | Branch Coverage |\n"
"|---|---|---|\n"
) + "\n".join(cov_rows) + "\n"
else:
md += "_No JaCoCo report found._\n"
with open("reports/test.md", "w") as out:
out.write(md)
print(md)
EOF
- name: Commit Test-Report to Repo
shell: bash
env:
GIT_USER: gitea-actions-bot
GIT_EMAIL: bot@mein-gateway.de
GITEA_TOKEN: ${{ inputs.token }}
run: |
git config user.name "$GIT_USER"
git config user.email "$GIT_EMAIL"
git remote set-url origin https://$GIT_USER:$GITEA_TOKEN@git.mein-gateway.de/${{ github.repository }}.git
git fetch origin
git checkout ${{ github.ref_name }}
git add reports/
git diff --cached --quiet && echo "No changes" && exit 0
git commit -m "ci: update test report [skip ci]"
git push origin ${{ github.ref_name }}
@@ -0,0 +1,21 @@
name: "Setup Maven with GitHub Packages"
description: "Sets up JDK and caches Maven dependencies."
inputs:
java-distribution:
description: "Java distribution to use"
default: "temurin"
java-version:
description: "Java version to use"
default: "17"
required: true
runs:
using: "composite"
steps:
- name: Set up JDK
with:
distribution: ${{ inputs.java-distribution }}
java-version: ${{ inputs.java-version }}
uses: actions/setup-java@v5
+34
View File
@@ -0,0 +1,34 @@
name: Build and Analyse
on:
push:
branches:
- develop
- feature*
pull_request:
types: [ opened, synchronize, reopened ]
defaults:
run:
shell: bash
jobs:
build-and-analyse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Setup Java and Maven
uses: ./.gitea/actions/setup-java-maven
- name: Build and test
run: mvn -B verify
- name: Publish Test Report
uses: ./.gitea/actions/publish-report
if: ${{ always() }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
+14
View File
@@ -0,0 +1,14 @@
/.settings/
/target/
/.classpath
/.project
/.factorypath
/.springBeans
/.idea
*.iml
# local files
/info.txt
cf.yml
/docs/apidocs/javadoc.sh
/.claude/settings.local.json
+16 -13
View File
@@ -1,18 +1,21 @@
MIT License MIT License
Copyright (c) 2026 thischwa Copyright (c) 2025 Thilo Schwarz
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy
associated documentation files (the "Software"), to deal in the Software without restriction, including of this software and associated documentation files (the "Software"), to deal
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell in the Software without restriction, including without limitation the rights
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
following conditions: copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial The above copyright notice and this permission notice shall be included in all
portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
USE OR OTHER DEALINGS IN THE SOFTWARE. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+414 -1
View File
@@ -1,3 +1,416 @@
# CloudflareDNS-java # CloudflareDNS-java
This project focuses on minimalistic Cloudflare API access, enabling DNS management tasks such as creating, updating, and deleting records for types like A, CNAME, MX, TXT, and others. [![Build and Analyse](https://git.mein-gateway.de/thischwa/CloudflareDNS-java/actions/workflows/build-and-analyse.yml/badge.svg?branch=develop)](https://git.mein-gateway.de/thischwa/CloudflareDNS-java/actions?workflow=build-and-analyse.yml)
## Preface
This project provides a java client for minimalistic access to the Cloudflare API version 4, which is mainly used for
managing DNS settings such as creating, updating and deleting DNS records.
If you encounter any bugs or find missing features, feel free to report them on
the [GitHub Issues page](https://github.com/th-schwarz/CloudflareDNS-java/issues).
---
## Disclaimer
This guide comes without any warranty. Use at your own risk. The author is not responsible for potential data loss, hardware damage or keyboard mishaps!
---
## Get It
The project has its own maven repository. Follow the instructions on the latest [package](https://git.mein-gateway.de/thischwa/-/packages/maven/codes.thischwa:cloudflaredns) to add the repository to your project.
## Changelog
See [changelog](changelog.md)
## Methods Overview
The methods can be categorized as follows:
- `Zone`: list, info
- `Record`: list, info, create, update, delete
The API provides two styles for working with DNS records:
1. **Traditional API**: Direct method calls with explicit parameters
2. **Fluent API**: Chainable method calls for more readable code
The following text focuses on the basic methods. For further information, take a look at
the \
[![javadoc](https://javadoc.mein-gateway.de/badge/CloudflareDNS-java)](https://javadoc.mein-gateway.de/CloudflareDNS-java/index.html)
### Instantiation of `CfDnsClient`
#### With API Token (recommended):
```java
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.build();
```
#### With Email/Key (legacy):
```java
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
.withEmailKeyAuth("email@example.com", "yourApiKey")
.build();
```
### `zoneList`
Retrieve all zones within the Cloudflare account.
- **Returns**: A list of `ZoneEntity` objects.
```java
List<ZoneEntity> zones = cfDnsClient.zoneList();
zones.forEach(zone -> System.out.println("Zone: " + zone.getName()));
```
---
### `zoneGet`
Get detailed information about a specific zone by its name.
- **Parameters**:
- `String name` - The zone name (e.g., "example.com").
- **Returns**: A `ZoneEntity` object.
```java
ZoneEntity zone = cfDnsClient.zoneGet("example.com");
System.out.println("Zone ID: " + zone.getId());
```
---
### `recordList`
Retrieve records for a given zone. There are two variants:
#### List by SLD
Retrieve all records for a specific second-level domain (SLD) under a given zone.
- **Parameters**:
- `ZoneEntity zone` - The zone object.
- `String sld` - Second-level domain (e.g., "www" in "www.example.com").
- **Returns**: A list of `RecordEntity` objects.
```java
List<RecordEntity> records = cfDnsClient.recordList(zone, "sld");
records.forEach(record ->
System.out.println("Record Type: "+record.getType()
+", Value: "+record.getContent())
);
```
#### List by Zone (optional filtering)
Retrieve DNS record details for a zone or a specific SLD, optionally filtered by record types.
- **Parameters**:
- `ZoneEntity zone` - The zone object.
- `String sld` - (Optional) The second-level domain.
- `RecordType... types` - Optional record types to filter by (e.g., A, CNAME). If not specified, returns all record types.
- **Returns**: A list of `RecordEntity` objects matching the criteria.
```java
// Get all records for a specific SLD
List<RecordEntity> allSldRecords = cfDnsClient.recordList(zone, "www");
// Get only A records for a specific SLD
List<RecordEntity> aSldRecords = cfDnsClient.recordList(zone, "www", RecordType.A);
// Get all records of a zone
List<RecordEntity> allZoneRecords = cfDnsClient.recordList(zone);
// Get only A and AAAA records of a zone
List<RecordEntity> ipZoneRecords = cfDnsClient.recordList(zone, RecordType.A, RecordType.AAAA);
```
---
### `recordCreate`
Create a new DNS record in a specific zone.
- **Parameters**:
- `ZoneEntity zone` - DNS zone object.
- `String sld` - The sub-tld of the new record.
- `int ttl` - The time-to-live in seconds of the new rcord.
```java
RecordEntity created = client.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.1");
System.out.println("Created Record ID: " + created.getId());
```
---
### `recordUpdate`
Update an existing DNS record.
- **Parameters**:
- `ZoneEntity zone` - The zone that contains the record.
- `RecordEntity rec` - Updated record data.
```java
record.setContent("192.168.1.2");
RecordEntity updated = cfDnsClient.recordUpdate(zone, record);
System.out.println("Updated Record: " + updated.getContent());
```
---
### `recordDelete`
Delete a DNS record from a zone.
- **Parameters**:
- `ZoneEntity zone` - The parent zone.
- `RecordEntity rec` - Record to delete.
```java
boolean isDeleted = cfDnsClient.recordDelete(zone, record);
System.out.println(isDeleted ? "Deletion successful." : "Deletion failed.");
```
---
### `recordDeleteTypeIfExists`
Delete a DNS record of a specific type if it exists.
- **Parameters**:
- `ZoneEntity zone` - Target zone.
- `String sld` - Second-level domain.
- `RecordType type` - Record type.
```java
cfDnsClient.recordDeleteTypeIfExists(zone, "api", RecordType.A);
System.out.println("Deletion attempt completed.");
```
### Batch Operations
Process multiple DNS record operations (POST, PUT, PATCH, DELETE) in a single batch request.
- **Parameters**:
- `ZoneEntity zone` - The target zone.
- `List<RecordEntity> postRecords` - Records to create (nullable). Can be built without IDs.
- `List<RecordEntity> putRecords` - Records to fully replace (nullable). **Requires record IDs**.
- `List<RecordEntity> patchRecords` - Records to partially update (nullable). **Requires record IDs**.
- `List<RecordEntity> deleteRecords` - Records to delete (nullable). **Requires record IDs**.
- **Returns**: A `BatchEntry` object containing the processed records.
**Important**: For UPDATE (PATCH), REPLACE (PUT), and DELETE operations, you must first retrieve the existing records to obtain their IDs.
#### Batch Create (POST)
Create new records, IDs are not required:
```java
List<RecordEntity> newRecords = Arrays.asList(
RecordEntity.build("cdn." + zone.getName(), RecordType.A, 60, "192.168.1.11"),
RecordEntity.build("mail." + zone.getName(), RecordType.A, 60, "192.168.1.12")
);
BatchEntry result = cfDnsClient.recordBatch(zone, newRecords, null, null, null);
System.out.println("Created "+result.getPosts().size() +" records.");
```
#### Batch Update (PATCH)
Partially update existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToUpdate = new ArrayList<>();
recordsToUpdate.add(cfDnsClient.recordList(zone, "api",RecordType.A).get(0));
recordsToUpdate.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
// Step 2: Modify only the fields you want to update
recordsToUpdate.forEach(record ->record.setContent("192.168.2.10"));
// Step 3: Send batch update request
BatchEntry result = cfDnsClient.recordBatch(zone, null, null, recordsToUpdate, null);
System.out.println("Updated "+result.getPatches().size() +" records.");
```
#### Batch Replace (PUT)
Fully replace existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToReplace = new ArrayList<>();
recordsToReplace.add(cfDnsClient.recordList(zone, "mail",RecordType.A).get(0));
recordsToReplace.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
// Step 2: Modify all fields as needed (full replacement)
recordsToReplace.get(0).setContent("192.168.3.10");
recordsToReplace.get(0).setTtl(120);
recordsToReplace.get(1).setContent("192.168.3.11");
recordsToReplace.get(1).setTtl(120);
// Step 3: Send batch replace request
BatchEntry result = cfDnsClient.recordBatch(zone, null, recordsToReplace, null, null);
System.out.println("Replaced "+result.getPuts().size() +" records.");
```
#### Batch Delete
Delete existing records. **Record IDs are required** - fetch them first:
```java
// Step 1: Fetch existing records to get their IDs
List<RecordEntity> recordsToDelete = new ArrayList<>();
recordsToDelete.add(cfDnsClient.recordList(zone, "cdn",RecordType.A).get(0));
recordsToDelete.add(cfDnsClient.recordList(zone, "mail",RecordType.A).get(0));
// Step 2: Send batch delete request
BatchEntry result = cfDnsClient.recordBatch(zone, null, null, null, recordsToDelete);
System.out.println("Deleted "+recordsToDelete.size() +" records.");
```
#### Combined Batch Operations
Combine multiple operations in a single batch request:
```java
// Create new records (no IDs needed)
List<RecordEntity> newRecords = Arrays.asList(
RecordEntity.build("new-api." + zone.getName(), RecordType.A, 60, "192.168.1.100")
);
// Fetch existing records for update/delete (IDs required)
List<RecordEntity> recordsToUpdate = Arrays.asList(
cfDnsClient.recordList(zone, "existing-api", RecordType.A).get(0)
);
recordsToUpdate.get(0).setContent("192.168.1.200");
List<RecordEntity> recordsToDelete = Arrays.asList(
cfDnsClient.recordList(zone, "old-api", RecordType.A).get(0)
);
// Execute all operations in a single batch request
BatchEntry result = cfDnsClient.recordBatch(
zone,
newRecords, // POST - create new records
null, // PUT - not used in this example
recordsToUpdate, // PATCH - update existing records
recordsToDelete // DELETE - remove records
);
System.out.println("Batch completed:");
System.out.println(" Created: "+result.getPosts().size());
System.out.println(" Updated: "+result.getPatches().size());
System.out.println(" Deleted: "+result.getDeletes().size());
```
---
## Fluent API
The fluent API provides a chainable, readable interface for DNS operations. It's an alternative to the traditional API
that reduces verbosity and improves code readability.
### Basic Usage
```java
// Create a DNS getRecord
client.zone("example.com")
.record("api")
.create(RecordType.A, "192.168.1.1",60);
// Get DNS records of a subdomain
List<RecordEntity> records = client.zone("example.com")
.record("www", RecordType.A)
.get();
// Get all DNS records of a zone
List<RecordEntity> zoneRecords = client.zone("example.com")
.list(RecordType.A, RecordType.AAAA);
// Update a DNS getRecord
RecordEntity updated = client.zone("example.com")
.record("api", RecordType.A)
.update("192.168.1.2");
// Delete DNS records
client.zone("example.com")
.record("old-service")
.delete(RecordType.A, RecordType.AAAA);
```
### Advantages of Fluent API
- **More Readable**: The chain of method calls reads like natural language
- **Less Verbose**: No need to pass zone objects between method calls
- **Type Safe**: Full compile-time type checking
- **IDE Friendly**: Excellent autocomplete support
### Complete Example
```java
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.build();
// Create a new getRecord
client.zone("example.com")
.record("api")
.create(RecordType.A, "192.168.100.1",60);
// Retrieve and verify
List<RecordEntity> records = client.zone("example.com")
.record("api", RecordType.A)
.get();
System.out.println("IP: "+records.get(0).getContent());
// Update the getRecord
client.zone("example.com")
.record("api",RecordType.A)
.update("192.168.100.2");
// Clean up
client.zone("example.com")
.record("api")
.delete(RecordType.A);
```
---
# Notes on Error Handling
The `CfDnsClient` provides internal error-handling mechanisms through exceptions. For example:
- `CloudflareApiException` is thrown for errors during API communication or invalid responses.
- `CloudflareNotFoundException` is thrown when the requested resource (single or multiple) is not found, if enabled via the `emptyResultThrowsException` flag during initialization. **Default is `false`**, meaning empty results will be returned without throwing an exception.
To enable exception throwing for empty results:
```java
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("your-api-token")
.withEmptyResultThrowsException(true)
.build();
```
## Example:
```java
try {
List<RecordEntity> records = cfDnsClient.recordList(zone, "www", RecordType.A);
System.out.println("Record IP: "+records.get(0).getContent());
} catch (CloudflareApiException e) {
if (e instanceof CloudflareNotFoundException) {
log.warn("Sld not found: www");
} else {
log.error("Error while getting sld info of www", e);
throw e;
}
}
```
---
+35
View File
@@ -0,0 +1,35 @@
# Changelog
- 0.5.0-SNAPSHOT:
- moved the project to git.mein-gateway.de
- replaced sonarqube with own actions
- migrate to jackson 3.x
- 0.4.0:
- fixed some paging issues
- **Breaking Change**: renamed `client.zone().record()` to `client.zone().getRecord()`
- Code quality improvements: Increasing test coverage
- 0.3.0:
- **Breaking Change**:
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`)
- Authentication with API token.
- 0.2.0:
- **Breaking Change**: `emptyResultThrowsException` default changed from `true` to `false`. Now applies to both
single and multiple result requests. Empty results will be returned by default without throwing exceptions.
- API method names refactored for consistency: `zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` →
`recordList`
- RecordEntity getter methods renamed for clarity: `getName()` → `getSld()`
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`) and added chainable method interface for more readable DNS operations (
`client.zone().record()...`)
- Code quality improvements: removed duplication in batch operations, improved type safety in HTTP methods,
optimized string concatenation, removed mutable setters from CfDnsClient
- Enhanced type validation in `RecordEntity.build()` with better error messages
- CfClient#recordList must return multiple RecordEntries
- add a missing source jar
- ResponseResultInfo#Errors: wrong object structure
- changing multiple records with put, post, patch and delete for dns-records
- 0.1.0:
- refactored / extended tests
- 0.1.0-beta.3:
- fixed json deserialization
- added logging of api errors
- 0.1.0-beta.1: 1st runnable version
+274
View File
@@ -0,0 +1,274 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>codes.thischwa</groupId>
<artifactId>cloudflaredns</artifactId>
<version>0.5.0</version>
<name>CloudflareDNS-java</name>
<inceptionYear>2025</inceptionYear>
<packaging>jar</packaging>
<issueManagement>
<url>https://github.com/th-schwarz/CloudflareDNS-java/issues</url>
<system>GitHub Issues</system>
</issueManagement>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<file.encoding>UTF-8</file.encoding>
<project.build.sourceEncoding>${file.encoding}</project.build.sourceEncoding>
<project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
<!-- checkstyle -->
<checkstyle.version>12.3.0</checkstyle.version>
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
<checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml
</checkstyle.config.location>
<checkstyle.includeTestResources>false</checkstyle.includeTestResources>
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
<checkstyle.failOnViolation>false</checkstyle.failOnViolation>
<checkstyle.propertyExpansion>config_loc=${basedir}
</checkstyle.propertyExpansion>
<checkstyle.consoleOutput>true</checkstyle.consoleOutput>
<linkXRef>false</linkXRef>
<!-- 3rd party dependencies -->
<jackson.version>3.2.1</jackson.version>
<httpclient5.version>5.6.2</httpclient5.version>
<lombok.version>1.18.46</lombok.version>
<slf4j.version>2.0.18</slf4j.version>
<logback-classic.version>1.5.38</logback-classic.version>
<junit5.version>5.14.2</junit5.version>
<mockito-junit5.version>5.23.0</mockito-junit5.version>
<lombok-maven-plugin.version>1.18.20.0</lombok-maven-plugin.version>
</properties>
<scm>
<developerConnection>scm:git:https://git.mein-gateway.de/thischwa/CloudflareDNS-java.git</developerConnection>
<connection>scm:git:https://git.mein-gateway.de/thischwa/CloudflareDNS-java.git</connection>
<url>https://git.mein-gateway.de/thischwa/CloudflareDNS-java</url>
<tag>v0.5.0</tag>
</scm>
<distributionManagement>
<repository>
<id>mygitea</id>
<url>https://git.mein-gateway.de/api/packages/thischwa/maven</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<snapshotRepository>
<id>mygitea</id>
<url>https://git.mein-gateway.de/api/packages/thischwa/maven</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</snapshotRepository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>26.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit5.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito-junit5.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<configuration>
<failOnError>false</failOnError>
<failOnWarnings>false</failOnWarnings>
<locale>en</locale>
<source>${java.version}</source>
</configuration>
<executions>
<execution>
<id>build javadoc jar</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<arguments>-DskipTests</arguments>
</configuration>
</plugin>
<plugin>
<!-- checkstyle -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle.plugin.version}</version>
<executions>
<execution>
<id>checkstyle-validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<!-- generates the code coverage report for sonar cube -->
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report-code-coverage</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<formats>XML</formats>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- to export test summary -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
</plugin>
<plugin>
<!-- delombok -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>${lombok-maven-plugin.version}</version>
<executions>
<execution>
<id>delombok-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>delombok</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDirectory>src/main/java</sourceDirectory>
<outputDirectory>${project.build.directory}/delombok</outputDirectory>
<addOutputDirectory>false</addOutputDirectory>
</configuration>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>attach-delomboked-sources</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classesDirectory>${project.build.directory}/delombok</classesDirectory>
<classifier>sources</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
+38
View File
@@ -0,0 +1,38 @@
# Test Report
_Generated on 2026-09-07 06:51:01 UTC_
**✅ All tests passed**
## Test Results
| | Tests | Passed | Failed | Skipped |
|---|---|---|---|---|
| **Total** | 90 | 90 | 0 | 0 |
### Details
| Status | Suite | Tests | Passed | Failed | Skipped |
|---|---|---|---|---|---|
| ✅ | codes.thischwa.cf.CfBasicHttpClientTest | 9 | 9 | 0 | 0 |
| ✅ | codes.thischwa.cf.CfClientPenTest | 0 | 0 | 0 | 0 |
| ✅ | codes.thischwa.cf.CfClientTest | 0 | 0 | 0 | 0 |
| ✅ | codes.thischwa.cf.CfDnsClientBuilderTest | 16 | 16 | 0 | 0 |
| ✅ | codes.thischwa.cf.CfDnsClientMockTest | 16 | 16 | 0 | 0 |
| ✅ | codes.thischwa.cf.CfRequestTest | 8 | 8 | 0 | 0 |
| ✅ | codes.thischwa.cf.ObjectMapperTest | 2 | 2 | 0 | 0 |
| ✅ | codes.thischwa.cf.ResponseValidatorTest | 8 | 8 | 0 | 0 |
| ✅ | codes.thischwa.cf.fluent.FluentApiTest | 15 | 15 | 0 | 0 |
| ✅ | codes.thischwa.cf.model.BatchEntryTest | 2 | 2 | 0 | 0 |
| ✅ | codes.thischwa.cf.model.PagingRequestTest | 4 | 4 | 0 | 0 |
| ✅ | codes.thischwa.cf.model.RecordEntityTest | 6 | 6 | 0 | 0 |
| ✅ | codes.thischwa.cf.model.RecordTypeTest | 3 | 3 | 0 | 0 |
| ✅ | codes.thischwa.cf.model.ZoneEntityTest | 1 | 1 | 0 | 0 |
## Coverage
| Package | Line Coverage | Branch Coverage |
|---|---|---|
| codes.thischwa.cf.fluent | 100% (25/25) | 100% (4/4) |
| codes.thischwa.cf.model | 100% (93/93) | 90% (9/10) |
| codes.thischwa.cf | 89% (249/279) | 71% (51/72) |
+440
View File
@@ -0,0 +1,440 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.org (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Authors: Max Vetrenko, Mauryan Kansara, Ruslan Diachenko, Roman Ivanov.
-->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="${org.checkstyle.google.severity}" default="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/filefilters/index.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="SuppressWarningsFilter"/>
<!-- https://checkstyle.org/filters/suppressionfilter.html -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->
<module name="SuppressWithNearbyTextFilter">
<property name="nearbyTextPattern"
value="CHECKSTYLE.SUPPRESS\: (\w+) for ([+-]\d+) lines"/>
<property name="checkPattern" value="$1"/>
<property name="lineRange" value="$2"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/checks/whitespace/index.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="140"/>
<property name="ignorePattern"
value="^package.*|^import.*|href\s*=\s*&quot;[^&quot;]*&quot;|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<module name="LeftCurly">
<property name="id" value="LeftCurlyEol"/>
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CATCH,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="LeftCurly">
<property name="id" value="LeftCurlyNl"/>
<property name="option" value="nl"/>
<property name="tokens"
value="LITERAL_CASE, LITERAL_DEFAULT"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- LITERAL_CASE, LITERAL_DEFAULT are reused in SWITCH_RULE -->
<property name="id" value="LeftCurlyNl"/>
<property name="query" value="//SWITCH_RULE/SLIST"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF, LITERAL_SWITCH, LITERAL_CASE, LITERAL_FINALLY"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppression is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
or preceding-sibling::*[last()][self::LCURLY]]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_RETURN,
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, LITERAL_FINALLY, DO_WHILE, ELLIPSIS,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_CATCH, LAMBDA,
LITERAL_YIELD, LITERAL_CASE, LITERAL_WHEN"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="allowEmptySwitchBlockStatements" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,
TYPE_EXTENSION_AND, LITERAL_WHEN"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks
may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="WhitespaceAround"/>
<property name="query" value="//*[self::LITERAL_IF or self::LITERAL_ELSE or self::STATIC_INIT
or self::LITERAL_TRY or self::LITERAL_CATCH]/SLIST[count(./*)=1]
| //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]
//*[self::RCURLY][parent::SLIST[count(./*)=1]]"/>
</module>
<module name="RegexpSinglelineJava">
<property name="format" value="\{[ ]+\}"/>
<property name="message" value="Empty blocks should have no spaces. Empty blocks
may only be represented as '{}' when not part of a
multi-block statement (4.1.3)"/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordComponentName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Record component name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="2"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<!-- Suppression for block code until we find a way to detect them properly
until https://github.com/checkstyle/checkstyle/issues/15769 -->
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="Indentation"/>
<property name="query" value="//SLIST[not(parent::CASE_GROUP)]/SLIST
| //SLIST[not(parent::CASE_GROUP)]/SLIST/RCURLY"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="0"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF"/>
</module>
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="ConstructorsDeclarationGrouping"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, CTOR_CALL,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
LABELED_STAT, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
TYPE_EXTENSION_AND "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph">
<property name="allowNewlineParagraph" value="false"/>
</module>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="protected"/>
<property name="allowMissingPropertyJavadoc" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MissingJavadocMethod"/>
<property name="query" value="//*[self::METHOD_DEF or self::CTOR_DEF
or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]
[ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF
or self::RECORD_DEF or self::ENUM_DEF]
[not(./MODIFIERS/LITERAL_PUBLIC)]]"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="protected"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
RECORD_DEF, ANNOTATION_DEF"/>
<property name="excludeScope" value="nothing"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MethodName"/>
<property name="query" value="//METHOD_DEF[
./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]
]/IDENT"/>
<property name="message" value="'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'"/>
</module>
<module name="SingleLineJavadoc"/>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<module name="SuppressWarningsHolder" />
<module name="SuppressionCommentFilter">
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)" />
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)" />
<property name="checkFormat" value="$1" />
</module>
<module name="SuppressWithNearbyCommentFilter">
<property name="commentFormat" value="CHECKSTYLE.SUPPRESS\: ([\w\|]+)"/>
<!-- $1 refers to the first match group in the regex defined in commentFormat -->
<property name="checkFormat" value="$1"/>
<!-- The check is suppressed in the next line of code after the comment -->
<property name="influenceFormat" value="1"/>
</module>
</module>
</module>
@@ -0,0 +1,193 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractResponse;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPatch;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.jetbrains.annotations.NotNull;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/**
* Abstract base class for creating HTTP clients to interact with the Cloudflare API. Provides
* methods for handling GET and POST requests and includes utilities for constructing HTTP clients,
* managing authentication, and handling JSON serialization.
*/
@Slf4j
abstract class CfBasicHttpClient {
private final String baseUrl;
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.
*
* @param baseUrl the base URL for the Cloudflare API
* @param auth the authentication mechanism to use
*/
CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfDnsClientBuilder.CfAuth auth) {
this.baseUrl = baseUrl;
this.auth = auth;
this.objectMapper = JsonConf.initObjectMapper();
}
private CloseableHttpClient createHttpClient() {
return HttpClients.custom().addRequestInterceptorFirst((request, context, execChain) -> {
request.addHeader(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip");
request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
if (request instanceof ClassicHttpRequest classicRequest) {
auth.applyAuth(classicRequest);
}
}).build();
}
private <T extends AbstractResponse> T executeRequest(ClassicHttpRequest request,
Class<T> responseType)
throws CloudflareApiException {
String logUri = null;
try (CloseableHttpClient client = createHttpClient()) {
ResultWrapper result = client.execute(request,
(ClassicHttpResponse response) -> new ResultWrapper(response.getCode(),
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
T respObj = objectMapper.readValue(result.responseBody, responseType);
if (!respObj.getResponseResultInfo().isSuccess()) {
log.error("API error.");
StringBuilder errorMessage = new StringBuilder("API error");
if (!respObj.getResponseResultInfo().getErrors().isEmpty()) {
errorMessage.append(": ");
respObj.getResponseResultInfo().getErrors().forEach(e -> {
log.error(" - {}", e.toString());
errorMessage.append(e).append("; ");
});
// Remove trailing "; "
if (errorMessage.toString().endsWith("; ")) {
errorMessage.setLength(errorMessage.length() - 2);
}
}
throw new CloudflareApiException(errorMessage.toString());
}
logUri = request.getRequestUri();
if (result.statusCode >= 200 && result.statusCode < 300) {
return respObj;
} else {
log.error("{} request failed for URL {}: Status {}", request.getMethod(), request.getUri(),
result.statusCode);
throw new CloudflareApiException(
request.getMethod() + " request failed with status code: " + result.statusCode);
}
} catch (JacksonException e) {
log.error("JSON parsing error for request to {}", logUri, e);
throw new CloudflareApiException("Error processing JSON response", e);
} catch (Exception e) {
throw new CloudflareApiException("Unexpected error!", e);
}
}
/**
* Sends a GET request to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T getRequest(String endpoint, Class<T> responseType)
throws CloudflareApiException {
HttpGet request = new HttpGet(buildUrl(endpoint));
return executeRequest(request, responseType);
}
/**
* Sends a DELETE request to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T deleteRequest(String endpoint, Class<T> responseType) throws CloudflareApiException {
HttpDelete request = new HttpDelete(buildUrl(endpoint));
return executeRequest(request, responseType);
}
/**
* Sends a POST request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T postRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPost request = new HttpPost(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/**
* Sends a PUT request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T putRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPut request = new HttpPut(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/**
* Sends a PATCH request with a payload to the given endpoint and maps the response.
*
* @param endpoint the API endpoint path
* @param requestPayload the payload to send
* @param responseType the expected response type class
* @param <T> the response type extending AbstractResponse
* @return the parsed response object
* @throws CloudflareApiException if an error occurs during the request
*/
<T extends AbstractResponse> T patchRequest(String endpoint,
Object requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPatch request = new HttpPatch(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/**
* Sets the JSON payload for a request.
*/
private void setRequestPayload(BasicClassicHttpRequest request,
Object requestPayload)
throws CloudflareApiException {
try {
String jsonPayload = objectMapper.writeValueAsString(requestPayload);
log.trace("Request methode [{}] payload: {}", request.getMethod(), jsonPayload);
request.setEntity(new StringEntity(jsonPayload,
ContentType.APPLICATION_JSON));
} catch (JacksonException e) {
throw new CloudflareApiException("Error serializing JSON payload", e);
}
}
private String buildUrl(String endpoint) {
return baseUrl + endpoint;
}
}
@@ -0,0 +1,504 @@
package codes.thischwa.cf;
import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.fluent.ZoneOperationsImpl;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.BatchResponse;
import codes.thischwa.cf.model.PagingRequest;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
/**
* CfDnsClient is a client interface to interact with Cloudflare DNS service. It allows managing DNS
* records and zones within the Cloudflare system, including creating, updating, retrieving, and
* deleting DNS records.
*
* <p>Example with API token authentication (recommended):
* <pre><code>
* // Create a new CfDnsClient instance with API token
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .build();
*
* // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneGet("example.com");
* System.out.println("Zone ID: " + zone.getId());
*
* // Retrieve records of a subdomain
* List&lt;RecordEntity&gt; records = cfDnsClient.recordList(zone, "sld");
* records.forEach(record ->
* System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
* );
*
* // Create a record for the subdomain "api"
* RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10");
* System.out.println("Created Record ID: " + created.getId());
* </code></pre>
*
* <p>Example with email/key authentication (legacy):
* <pre><code>
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withEmailKeyAuth("email@example.com", "your-api-key")
* .build();
* </code></pre>
*
* <p>Example with exception throwing enabled:
* <pre><code>
* // Throws exception when results are empty
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .withEmptyResultThrowsException(true)
* .build();
* </code></pre>
*
* <p>Example with custom base URL:
* <pre><code>
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
* .withApiTokenAuth("your-api-token")
* .withBaseUrl("https://custom-api.example.com")
* .build();
* </code></pre>
*/
@Slf4j
public class CfDnsClient extends CfBasicHttpClient {
private final ResponseValidator responseValidator;
private final boolean emptyResultThrowsException;
/**
* Constructs a new instance of {@code CfDnsClient}.
*
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param baseUrl The base URL for the Cloudflare API endpoint.
* @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfDnsClientBuilder.CfAuth auth) {
super(baseUrl, auth);
this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException;
}
private static String buildFqdn(ZoneEntity zone, String sld) {
return sld + "." + zone.getName();
}
/**
* Groups a list of DNS records by their fully qualified domain name (FQDN).
*
* @param records A list of {@link RecordEntity} objects to be grouped by FQDN.
* @return A map where the key is the FQDN (name field) and the value is a list of {@link RecordEntity}
* objects that share that FQDN.
*/
public static Map<String, List<RecordEntity>> groupRecordsByFqdn(List<RecordEntity> records) {
if (records == null) {
return new HashMap<>();
}
return records.stream()
.collect(Collectors.groupingBy(RecordEntity::getName));
}
/**
* Provides fluent API access to operations in a specific zone.
* This method returns a ZoneOperations interface that allows chaining operations
* on DNS records within the specified zone.
*
* <p>Example:
* <pre><code>
* client.zone("example.com")
* .getRecord("api")
* .create(RecordType.A, "192.168.1.1", 60);
* </code></pre>
*
* @param zoneName the name of the DNS zone (e.g., "example.com")
* @return a ZoneOperations instance for chaining operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
public ZoneOperations zone(String zoneName) throws CloudflareApiException {
ZoneEntity zoneEntity = zoneGet(zoneName);
return new ZoneOperationsImpl(this, zoneEntity);
}
/**
* Retrieves a list of all zones from the Cloudflare API.
*
* @return A list of ZoneEntity objects representing the zones retrieved from the Cloudflare API.
* @throws CloudflareApiException If an error occurs during the API request or response handling.
*/
public List<ZoneEntity> zoneList() throws CloudflareApiException {
return zoneList(PagingRequest.defaultPaging());
}
/**
* Retrieves a list of all DNS zones using the provided paging request parameters.
*
* @param pagingRequest the pagination request object containing parameters for paging and
* filtering zone data
* @return a list of {@code ZoneEntity} objects representing the DNS zones retrieved from the API
* @throws CloudflareApiException if there is an error during the API request or response
* processing
*/
public List<ZoneEntity> zoneList(PagingRequest pagingRequest) throws CloudflareApiException {
String endpoint = pagingRequest.addQueryString(CfRequest.ZONE_LIST.buildPath());
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response);
return response.getResult();
}
/**
* Retrieves detailed information about a specific zone by its name.
*
* @param name The name of the zone to retrieve information for.
* @return A {@link ZoneEntity} object that contains details of the specified zone.
* @throws CloudflareApiException If an error occurs while making the API request or processing
* the response.
*/
public ZoneEntity zoneGet(String name) throws CloudflareApiException {
String endpoint = CfRequest.ZONE_INFO.buildPath(name);
ZoneMultipleResponse response = getRequest(endpoint, ZoneMultipleResponse.class);
checkResponse(response, true);
return response.getResult().get(0);
}
/**
* Retrieves a list of DNS records for a specified zone, with optional paging support.
*
* @param zone The zone entity containing information about the target zone.
* @return A list of RecordEntity objects representing the DNS records of the specified zone.
* @throws CloudflareApiException If an error occurs during the API request or response processing.
*/
public List<RecordEntity> recordList(ZoneEntity zone) throws CloudflareApiException {
return recordList(zone, (PagingRequest) null);
}
/**
* Retrieves a list of DNS records for a specified zone, with optional paging support.
*
* @param zone The zone entity containing information about the target zone.
* @param pagingRequest The paging request containing parameters such as page size and number.
* @return A list of RecordEntity objects representing the DNS records of the specified zone.
* @throws CloudflareApiException If an error occurs during the API request or response processing.
*/
public List<RecordEntity> recordList(ZoneEntity zone, @Nullable PagingRequest pagingRequest) throws CloudflareApiException {
PagingRequest pr = pagingRequest == null ? PagingRequest.defaultPaging() : pagingRequest;
String endpoint = pr.addQueryString(CfRequest.RECORD_LIST.buildPath(zone.getId()));
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp);
return resp.getResult();
}
/**
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
*
* @param zone the zone entity representing the DNS zone to query
* @param sld the second-level domain (SLD) to filter the records
* @return a list of RecordEntity objects that match the specified SLD within the zone
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld) throws CloudflareApiException {
return recordList(zone, sld, (RecordType[]) null);
}
/**
* Retrieves DNS records for the specified second-level domain (SLD) within a zone.
* Optionally, filters by one or more DNS 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 getRecord types to filter the results.
* @return A list of {@code RecordEntity} objects representing the DNS records for the specified domain.
* @throws CloudflareNotFoundException if the specified SLD is not found in the zone
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, String sld, @Nullable RecordType... types)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
String endpoint = CfRequest.RECORD_LIST_NAME.buildPath(zone.getId(), fqdn);
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp, false);
List<RecordEntity> recs = resp.getResult();
return filterAndSetZoneRecords(zone, types, recs);
}
/**
* Retrieves a list of all DNS records for a given zone.
* Optionally, filters by one or more DNS getRecord types.
*
* @param zone The zone entity containing information about the domain zone.
* @param types Optional parameter specifying one or more DNS getRecord types to filter the results.
* @return A list of {@code RecordEntity} objects representing the DNS records for the specified zone.
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public List<RecordEntity> recordList(ZoneEntity zone, RecordType... types)
throws CloudflareApiException {
String endpoint = CfRequest.RECORD_LIST.buildPath(zone.getId());
RecordMultipleResponse resp = getRequest(endpoint, RecordMultipleResponse.class);
checkResponse(resp, false);
List<RecordEntity> recs = resp.getResult();
return filterAndSetZoneRecords(zone, types, recs);
}
/**
* Creates a new DNS getRecord for a given second-level domain (SLD) within the specified zone.
*
* @param zone The ZoneEntity representing the DNS zone where the getRecord is to be created.
* @param sld The second-level domain (SLD) for which the DNS getRecord is being created.
* @param ttl The time-to-live (TTL) value for the DNS getRecord in seconds.
* @param type The RecordType specifying the type of the DNS getRecord (e.g., A, AAAA, CNAME).
* @param content The content of the DNS getRecord (e.g., IP address for A/AAAA records, target
* domain for CNAME).
* @return The created RecordEntity object containing details of the newly created DNS getRecord.
* @throws CloudflareApiException If an error occurs while communicating with the Cloudflare API
* or creating the getRecord.
*/
public RecordEntity recordCreateSld(ZoneEntity zone, String sld, int ttl, RecordType type,
String content) throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
return recordCreate(zone, fqdn, ttl, type, content);
}
/**
* Creates a DNS getRecord in the specified DNS zone with the provided details.
*
* @param zone the DNS zone in which the getRecord will be created
* @param name the name of the DNS getRecord (e.g., www.example.com)
* @param ttl the time-to-live (TTL) value for the DNS getRecord
* @param type the type of the DNS getRecord (e.g., A, AAAA, CNAME)
* @param content the content or value of the DNS getRecord
* @return the created DNS getRecord as a {@link RecordEntity} object
* @throws CloudflareApiException if an error occurs while interacting with the Cloudflare API
*/
public RecordEntity recordCreate(ZoneEntity zone, String name, int ttl, RecordType type,
String content) throws CloudflareApiException {
RecordEntity rec = RecordEntity.build(name, type, ttl, content);
return recordCreate(zone, rec);
}
/**
* Creates a new DNS getRecord in the specified zone using the Cloudflare API.
*
* @param zone The zone entity where the getRecord will be created. Contains details such as zone
* ID.
* @param rec The getRecord entity representing the DNS getRecord to be created, including its
* attributes.
* @return The created getRecord entity as returned by the Cloudflare API.
* @throws CloudflareApiException If an error occurs while interacting with the Cloudflare API.
*/
public RecordEntity recordCreate(ZoneEntity zone, RecordEntity rec)
throws CloudflareApiException {
String endpoint = CfRequest.RECORD_CREATE.buildPath(zone.getId());
RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType());
RecordEntity retRec = resp.getResult();
retRec.setZoneId(zone.getId());
return retRec;
}
/**
* 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 getRecord exists.
* @param rec The getRecord entity that represents the DNS getRecord to be deleted.
* @return {@code true} if the DNS getRecord was successfully deleted; {@code false} otherwise.
* @throws CloudflareApiException if there is an issue during the API communication, or the
* request fails for any reason.
*/
public boolean recordDelete(ZoneEntity zone, RecordEntity rec) throws CloudflareApiException {
boolean changed = recordDelete(zone, rec.getId());
if (changed) {
log.debug("Record {} of the type [{}] successful deleted.", rec.getSld(), rec.getType());
} else {
log.warn("Record {} of the type [{}] was not deleted.", rec.getSld(), rec.getType());
}
return changed;
}
/**
* 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 getRecord exists.
* @param id The getRecord entity that represents the DNS getRecord to be deleted.
* @return {@code true} if the DNS getRecord was successfully deleted; {@code false} otherwise.
* @throws CloudflareApiException if there is an issue during the API communication or the request
* fails for any reason.
*/
public boolean recordDelete(ZoneEntity zone, String id) throws CloudflareApiException {
String endpoint = CfRequest.RECORD_DELETE.buildPath(zone.getId(), id);
RecordSingleResponse resp = deleteRequest(endpoint, RecordSingleResponse.class);
checkResponse(resp);
log.debug("Record id#{} successful deleted.", id);
return resp.getResult().getId().equals(id);
}
/**
* 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 getRecord entity containing the ID of the DNS getRecord to be updated and its updated
* data
* @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)
throws CloudflareApiException {
// reset all dates, it causes an API issue
rec.setModifiedOn(null);
rec.setCreatedOn(null);
String endpoint = CfRequest.RECORD_UPDATE.buildPath(zone.getId(), rec.getId());
RecordSingleResponse resp = patchRequest(endpoint, rec, RecordSingleResponse.class);
checkResponse(resp);
log.info("Record {} of type {} successful updated.", rec.getSld(), rec.getType());
return resp.getResult();
}
/**
* Deletes DNS records of a specific type within a given zone if they exist. If no getRecord of the
* specified type exists, it logs this occurrence without throwing an exception.
*
* @param zone The DNS zone entity in which the getRecord exists.
* @param sld The second-level domain for which the getRecord is being checked.
* @param recordTypes The types of DNS records that should be deleted if they exist.
* @throws CloudflareApiException If an error occurs during API communication.
*/
public void recordDeleteTypeIfExists(ZoneEntity zone, String sld, RecordType... recordTypes)
throws CloudflareApiException {
String fqdn = buildFqdn(zone, sld);
List<RecordEntity> recs;
try {
recs = recordList(zone, sld, recordTypes);
} catch (CloudflareNotFoundException e) {
log.trace("No getRecord of type {} found for domain {}.", recordTypes, fqdn);
return;
}
for (RecordEntity rec : recs) {
try {
recordDelete(zone, rec);
log.info("Record {} of type {} successful deleted.", fqdn, recordTypes);
} catch (CloudflareApiException e) {
log.error("Failed to delete getRecord {} of type {} for zone {}: {}", fqdn, recordTypes, zone.getName(), e.getMessage());
}
}
}
/**
* Processes a batch of DNS getRecord operations (POST, PUT, PATCH, DELETE) for a specified zone.
* This method builds and cleans the input records, sends the batch request to the Cloudflare API,
* and returns a result containing processed batch entries.
*
* @param zone The zone entity to which the records belong.
* @param postRecords A list of DNS records to be created (POST). This parameter is nullable.
* @param putRecords A list of DNS records to be fully replaced (PUT). This parameter is nullable.
* @param patchRecords A list of DNS records to be partially updated (PATCH). This parameter is nullable.
* @param deleteRecords A list of DNS records to be deleted (DELETE). This parameter is nullable.
* @return The resulting {@link BatchEntry} containing the processed records after the batch operation.
* @throws CloudflareApiException If an error occurs while communicating with the Cloudflare API.
*/
public BatchEntry recordBatch(ZoneEntity zone, @Nullable List<RecordEntity> postRecords, @Nullable List<RecordEntity> putRecords,
@Nullable List<RecordEntity> patchRecords, @Nullable List<RecordEntity> deleteRecords)
throws CloudflareApiException {
BatchEntry batchEntry = new BatchEntry();
// build 'clean' getRecord entries
if (postRecords != null) {
batchEntry.setPosts(cleanRecordsForPostOrPut(postRecords));
}
if (putRecords != null) {
batchEntry.setPuts(cleanRecordsForPostOrPut(putRecords));
}
if (patchRecords != null) {
batchEntry.setPatches(cleanRecordsForPatch(patchRecords));
}
if (deleteRecords != null) {
batchEntry.setDeletes(cleanRecordsForDelete(deleteRecords));
}
String endpoint = CfRequest.RECORD_BATCH.buildPath(zone.getId());
BatchResponse resp = postRequest(endpoint, batchEntry, BatchResponse.class);
checkResponse(resp);
// set zone id
BatchEntry result = resp.getResult();
setZoneIdForBatchResults(result, zone.getId());
return result;
}
private List<RecordEntity> filterAndSetZoneRecords(ZoneEntity zone, @Nullable RecordType[] types, List<RecordEntity> recs)
throws CloudflareNotFoundException {
List<RecordEntity> filtered;
if (types != null && types.length > 0) {
Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types));
filtered = recs.stream()
.filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType()))).toList();
} else {
filtered = new ArrayList<>(recs);
}
filtered.forEach(rec -> rec.setZoneId(zone.getId()));
// special exception for an empty result, normally it's done in the RecordValidator
if (filtered.isEmpty() && emptyResultThrowsException) {
throw new CloudflareNotFoundException("No records exist after filtering zone: " + zone.getName());
}
return filtered;
}
private List<RecordEntity> cleanRecordsForPostOrPut(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), rec.getTtl(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForPatch(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getContent())));
return cleaned;
}
private List<RecordEntity> cleanRecordsForDelete(List<RecordEntity> records) {
List<RecordEntity> cleaned = new ArrayList<>();
records.forEach(
rec -> cleaned.add(RecordEntity.build(rec.getId(), rec.getSld(), rec.getType(), null, rec.getContent())));
return cleaned;
}
private void setZoneIdForBatchResults(BatchEntry result, String zoneId) {
if (result.getPosts() != null) {
result.getPosts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPuts() != null) {
result.getPuts().forEach(rec -> rec.setZoneId(zoneId));
}
if (result.getPatches() != null) {
result.getPatches().forEach(rec -> rec.setZoneId(zoneId));
}
}
private void checkResponse(AbstractResponse resp) throws CloudflareApiException {
checkResponse(resp, false);
}
private void checkResponse(AbstractResponse resp, boolean singleResultExpected)
throws CloudflareApiException {
responseValidator.validate(resp, singleResultExpected);
}
}
@@ -0,0 +1,177 @@
package codes.thischwa.cf;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Builder class for configuring and creating instances of {@link CfDnsClient}.
* This class provides a fluent API for customizing the client settings,
* such as the base URL and authentication mechanism.
*/
public class CfDnsClientBuilder {
/**
* The default base URL for the Cloudflare v4 API requests made by the {@code CfDnsClient}.
*/
public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
private boolean emptyResultThrowsException;
private CfAuth auth;
@Nullable
private String baseUrl;
/**
* Configures whether an exception should be thrown when an empty result is encountered
* during operations performed by the `CfDnsClient`.
*
* @param emptyResultThrowsException a boolean flag indicating if an exception should be thrown
* when an empty result is returned. If set to `true`, operations
* that result in an empty response will throw an exception;
* otherwise, they will not.
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
* to further configure the builder.
*/
public CfDnsClientBuilder withEmptyResultThrowsException(boolean emptyResultThrowsException) {
this.emptyResultThrowsException = emptyResultThrowsException;
return this;
}
/**
* Sets the base URL to be used by the {@code CfDnsClient}.
* This method allows configuring the base URL for API requests, overriding any default value.
*
* @param baseUrl the base URL to be used for API requests
* @return the current instance of {@code CfDnsClientBuilder}, enabling method chaining
*/
public CfDnsClientBuilder withBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
/**
* Configures the authentication method for the {@code CfDnsClient} to use an API token.
* This is the recommended way to authenticate with the Cloudflare API, as it provides
* enhanced security and ease of use compared to other authentication mechanisms.
*
* @param apiToken the Cloudflare API token. This token is required for authenticating
* API requests and must not be null or blank.
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
* to further configure the builder.
* @throws IllegalArgumentException if the {@code apiToken} is null or blank.
*/
public CfDnsClientBuilder withApiTokenAuth(String apiToken) {
this.auth = new ApiTokenAuth(apiToken);
return this;
}
/**
* Configures the authentication method for the {@code CfDnsClient} to use an email and API key.
* This approach uses the legacy authentication mechanism provided by Cloudflare, where requests
* are authenticated with a combination of an account's email address and API key.
*
* @param authEmail the email address associated with the Cloudflare account. This must not be null or blank.
* @param authKey the API key of the Cloudflare account. This must not be null or blank.
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
* to further configure the builder.
* @throws IllegalArgumentException if {@code authEmail} or {@code authKey} is null or blank.
*/
public CfDnsClientBuilder withEmailKeyAuth(String authEmail, String authKey) {
this.auth = new EmailKeyAuth(authEmail, authKey);
return this;
}
/**
* Builds and returns a configured instance of {@code CfDnsClient}.
*
* <p>The method constructs a new {@code CfDnsClient} object based on the
* options set in the {@code CfDnsClientBuilder}. If no base URL has been
* explicitly configured, a default base URL will be used.
*
* @return a new instance of {@code CfDnsClient} configured with the
* specified options such as base URL, authentication details,
* and the exception-handling policy for empty results.
*/
public CfDnsClient build() {
String url = baseUrl == null ? DEFAULT_BASEURL : baseUrl;
return new CfDnsClient(emptyResultThrowsException, url, auth);
}
/**
* Interface for Cloudflare authentication mechanisms.
* Implementations of this interface provide different methods of authentication
* with the Cloudflare API (e.g., API token, email/key combination).
*/
interface CfAuth {
/**
* Applies authentication headers to the given HTTP request.
*
* @param request the HTTP request to authenticate
*/
void applyAuth(ClassicHttpRequest request);
}
/**
* Authentication mechanism using Cloudflare API token.
* This is the recommended authentication method for the Cloudflare API.
*/
static class ApiTokenAuth implements CfAuth {
private final String apiToken;
/**
* Creates a new API token authentication object.
*
* @param apiToken the Cloudflare API token
* @throws IllegalArgumentException if the API token is null or blank
*/
ApiTokenAuth(@NotNull String apiToken) {
if (apiToken.isBlank()) {
throw new IllegalArgumentException("API token must not be null or blank!");
}
this.apiToken = apiToken;
}
@Override
public void applyAuth(ClassicHttpRequest request) {
request.addHeader("Authorization", "Bearer " + apiToken);
}
}
/**
* Authentication mechanism using Cloudflare account email and API key.
* This is the legacy authentication method for the Cloudflare API.
*/
static class EmailKeyAuth implements CfAuth {
private final String authEmail;
private final String authKey;
/**
* Creates a new email/key authentication object.
*
* @param authEmail the email address associated with the Cloudflare account
* @param authKey the API key of the Cloudflare account
* @throws IllegalArgumentException if email or key is null or blank
*/
EmailKeyAuth(@NotNull String authEmail, @NotNull String authKey) {
if (authEmail.isBlank()) {
throw new IllegalArgumentException("Authentication email must not be null or blank!");
}
if (authKey.isBlank()) {
throw new IllegalArgumentException("Authentication key must not be null or blank!");
}
this.authEmail = authEmail;
this.authKey = authKey;
}
@Override
public void applyAuth(ClassicHttpRequest request) {
request.addHeader("X-Auth-Email", authEmail);
request.addHeader("X-Auth-Key", authKey);
}
}
}
@@ -0,0 +1,85 @@
package codes.thischwa.cf;
import lombok.Getter;
/**
* Enum CfRequest encapsulates various API endpoint paths for managing DNS zones and records in a
* cohesive and reusable manner. Each enum constant represents a specific API request path.
*/
@Getter
public enum CfRequest {
/**
* Represents the API endpoint path for retrieving the list of DNS zones.
*/
ZONE_LIST("/zones"),
/**
* Represents the API endpoint path for retrieving information about a specific DNS zone by its
* name. The endpoint path supports a placeholder for the zone name, which needs to be provided to
* construct the complete path.
*/
ZONE_INFO("/zones?name=%s"),
/**
* Represents the API endpoint path for retrieving information about DNS records within a
* specific DNS zone. The endpoint path includes a placeholder for the zone identifier, which
* needs to be provided to construct the complete path.
*/
RECORD_LIST("/zones/%s/dns_records"),
/**
* Represents the API endpoint path for 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 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.
* This constant is used to construct the URL for interacting with the batch DNS records API.
*/
RECORD_BATCH("/zones/%s/dns_records/batch"),
/**
* Represents the API endpoint path for deleting an existing DNS getRecord within a specific DNS
* zone. The endpoint path includes placeholders for the zone identifier and the getRecord
* identifier, which need to be provided to construct the complete path.
*/
RECORD_DELETE("/zones/%s/dns_records/%s");
private static final char VAR_IDENTIFICATION = '%';
private final String path;
CfRequest(String path) {
this.path = path;
}
/**
* Constructs the complete API endpoint path by formatting the base path with the provided
* arguments.
*
* @param vars the arguments to format the path string with; these are typically specific
* identifiers or parameters required by the API endpoint.
* @return the fully constructed API endpoint path as a string.
*/
String buildPath(Object... vars) {
long varCount = path.chars().filter(c -> c == VAR_IDENTIFICATION).count();
if (varCount != vars.length) {
throw new IllegalArgumentException(
String.format(
"The number of variables (%d) does not match the number of parameters (%d) in the path string: %s",
vars.length, varCount, path));
}
return String.format(path, vars);
}
}
@@ -0,0 +1,33 @@
package codes.thischwa.cf;
import java.io.Serial;
/**
* Represents a custom exception for errors encountered while interacting with the Cloudflare API.
*/
public class CloudflareApiException extends Exception {
@Serial
private static final long serialVersionUID = 1L;
/**
* Constructs a new CloudflareApiException with the specified detail message.
*
* @param message the detail message, which provides more information about the exception.
*/
public CloudflareApiException(String message) {
super(message);
}
/**
* Constructs a new CloudflareApiException with the specified detail message and cause.
*
* @param message the detail message, which provides additional context or information about the
* exception.
* @param cause the cause of this exception, which is the underlying throwable that triggered this
* exception.
*/
public CloudflareApiException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,21 @@
package codes.thischwa.cf;
/**
* This exception is thrown to indicate that a requested resource was not found during interaction
* with the Cloudflare API.
*
* <p>It extends {@link CloudflareApiException} to provide specific errors related to situations
* where Cloudflare responds with a "not found" operation.
*/
public class CloudflareNotFoundException extends CloudflareApiException {
/**
* Constructs a new CloudflareNotFoundException with the specified detail message.
*
* @param message the detail message, which provides additional context about the "not found"
* error encountered during interaction with the Cloudflare API.
*/
public CloudflareNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,26 @@
package codes.thischwa.cf;
import com.fasterxml.jackson.annotation.JsonInclude;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.json.JsonMapper;
/**
* The JsonConf class provides a utility method for initializing and configuring a shared
* {@link ObjectMapper} instance for JSON serialization and deserialization.
*/
class JsonConf {
private JsonConf() {
}
static ObjectMapper initObjectMapper() {
return JsonMapper.builder()
.changeDefaultPropertyInclusion(
incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.build();
}
}
@@ -0,0 +1,73 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.AbstractSingleResponse;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import java.util.stream.Collectors;
/**
* Validates API responses to ensure compliance with expected conditions, such as response success
* and result count.
*
* <p>This class performs two primary validation tasks:
* <ul>
* <li>It checks whether the API response was successful by analyzing the associated response
* metadata. If the response indicates failure, an exception is thrown with descriptive error
* messages.
* <li>It validates the number of results in the API response payload to detect unexpected counts.
* For {@link RecordMultipleResponse}, it checks if results are empty or if more than one result
* was returned when a single result was expected. For {@link AbstractSingleResponse}, it checks
* if the result is null. Depending on the parameter 'emptyResultThrowsException', an exception
* will be triggered or an empty/null result will be returned.
* </ul>
*/
class ResponseValidator {
private final boolean emptyResultThrowsException;
ResponseValidator(boolean emptyResultThrowsException) {
this.emptyResultThrowsException = emptyResultThrowsException;
}
void validate(AbstractResponse resp, boolean singleResultExpected) throws CloudflareApiException {
validateResponseSuccess(resp);
validateResultCount(resp, singleResultExpected);
}
private void validateResponseSuccess(AbstractResponse resp) throws CloudflareApiException {
ResponseResultInfo resultInfo = resp.getResponseResultInfo();
if (!resultInfo.isSuccess()) {
String errors =
resultInfo.getErrors().stream().map(Object::toString).collect(Collectors.joining(", "));
throw new CloudflareApiException("Error in response: " + errors);
}
}
private void validateResultCount(AbstractResponse resp, boolean singleResultExpected)
throws CloudflareApiException {
if (resp instanceof RecordMultipleResponse respMulti) {
validateMultipleResponse(respMulti, singleResultExpected);
} else if (resp instanceof AbstractSingleResponse<?> respSingle) {
validateSingleResponse(respSingle);
}
}
private void validateMultipleResponse(RecordMultipleResponse response, boolean singleResultExpected)
throws CloudflareApiException {
int totalCount = response.getResultInfo().totalCount();
if (singleResultExpected && totalCount > 1) {
throw new CloudflareApiException("Unexpected result count: " + totalCount);
}
if (emptyResultThrowsException && totalCount == 0) {
throw new CloudflareNotFoundException("No result found");
}
}
private void validateSingleResponse(AbstractSingleResponse<?> response)
throws CloudflareNotFoundException {
if (emptyResultThrowsException && response.getResult() == null) {
throw new CloudflareNotFoundException("No result found");
}
}
}
@@ -0,0 +1,49 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import java.util.List;
/**
* Fluent interface for getRecord-level operations.
* Provides a chainable API for CRUD operations on DNS records.
*/
public interface RecordOperations {
/**
* Retrieves DNS records for the selected subdomain.
*
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
List<RecordEntity> get() throws CloudflareApiException;
/**
* Creates a new DNS getRecord with the specified parameters.
*
* @param type the DNS getRecord type (e.g., A, AAAA, CNAME)
* @param content the content of the DNS getRecord (e.g., IP address)
* @param ttl the time-to-live value in seconds
* @return the created RecordEntity
* @throws CloudflareApiException if an error occurs while creating the getRecord
*/
RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException;
/**
* Updates an existing DNS getRecord with new content.
*
* @param newContent the new content for the DNS getRecord
* @return the updated RecordEntity
* @throws CloudflareApiException if an error occurs while updating the getRecord
*/
RecordEntity update(String newContent) throws CloudflareApiException;
/**
* Deletes DNS records of the specified types.
*
* @param types the DNS getRecord types to delete
* @throws CloudflareApiException if an error occurs while deleting records
*/
void delete(RecordType... types) throws CloudflareApiException;
}
@@ -0,0 +1,64 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of RecordOperations for fluent API access to getRecord-level operations.
*/
public class RecordOperationsImpl implements RecordOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
private final String sld;
private final RecordType[] types;
/**
* Constructs a RecordOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
* @param sld the subdomain (second-level domain) name
* @param types optional array of RecordType to filter by
*/
public RecordOperationsImpl(CfDnsClient client, ZoneEntity zone, String sld, @Nullable RecordType[] types) {
this.client = client;
this.zone = zone;
this.sld = sld;
this.types = types;
}
@Override
public List<RecordEntity> get() throws CloudflareApiException {
return client.recordList(zone, sld, types);
}
@Override
public RecordEntity create(RecordType type, String content, int ttl) throws CloudflareApiException {
return client.recordCreateSld(zone, sld, ttl, type, content);
}
@Override
public RecordEntity update(String newContent) throws CloudflareApiException {
List<RecordEntity> recs = get();
if (recs.isEmpty()) {
throw new CloudflareApiException("No recs found to update for subdomain: " + sld);
}
if (recs.size() > 1) {
throw new CloudflareApiException("Multiple recs found. Please use recordUpdate() directly for precise control.");
}
RecordEntity rec = recs.get(0);
rec.setContent(newContent);
return client.recordUpdate(zone, rec);
}
@Override
public void delete(RecordType... types) throws CloudflareApiException {
client.recordDeleteTypeIfExists(zone, sld, types);
}
}
@@ -0,0 +1,40 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordType;
import org.jetbrains.annotations.Nullable;
/**
* Fluent interface for zone-level operations.
* Provides a chainable API for accessing and manipulating DNS records within a specific zone.
*/
public interface ZoneOperations {
/**
* Selects a record (subdomain) within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations getRecord(String sld) throws CloudflareApiException;
/**
* Selects a record with specific types within the zone for further operations.
*
* @param sld the second-level domain (subdomain) name
* @param types optional DNS record types to filter by
* @return a RecordOperations instance for chaining record-specific operations
* @throws CloudflareApiException if the zone cannot be found or accessed
*/
RecordOperations getRecord(String sld, @Nullable RecordType... types) throws CloudflareApiException;
/**
* Lists all DNS records within the zone, optionally filtered by types.
*
* @param types optional DNS record types to filter by
* @return a list of RecordEntity objects matching the criteria
* @throws CloudflareApiException if an error occurs while retrieving records
*/
java.util.List<codes.thischwa.cf.model.RecordEntity> list(@Nullable RecordType... types) throws CloudflareApiException;
}
@@ -0,0 +1,44 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of ZoneOperations for fluent API access to zone-level operations.
*/
public class ZoneOperationsImpl implements ZoneOperations {
private final CfDnsClient client;
private final ZoneEntity zone;
/**
* Constructs a ZoneOperationsImpl instance.
*
* @param client the CfDnsClient instance to use for operations
* @param zone the ZoneEntity representing the DNS zone
*/
public ZoneOperationsImpl(CfDnsClient client, ZoneEntity zone) {
this.client = client;
this.zone = zone;
}
@Override
public RecordOperations getRecord(String sld) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, null);
}
@Override
public RecordOperations getRecord(String sld, @Nullable RecordType... types) throws CloudflareApiException {
return new RecordOperationsImpl(client, zone, sld, types);
}
@Override
public List<RecordEntity> list(@Nullable RecordType... types) throws CloudflareApiException {
return client.recordList(zone, types);
}
}
@@ -0,0 +1,31 @@
/**
* Fluent API interfaces and implementations for chainable DNS operations.
*
* <p>This package provides a fluent, chainable interface for interacting with Cloudflare DNS
* records, making code more readable and concise.
*
* <p>Example usage:
* <pre><code>
* // Create a DNS getRecord
* client.zone("example.com")
* .getRecord("api")
* .create(RecordType.A, "192.168.1.1", 60);
*
* // Get DNS records
* List&lt;RecordEntity&gt; records = client.zone("example.com")
* .getRecord("www", RecordType.A)
* .get();
*
* // Update a DNS getRecord
* client.zone("example.com")
* .getRecord("api", RecordType.A)
* .update("192.168.1.2");
*
* // Delete DNS records
* client.zone("example.com")
* .getRecord("old-service")
* .delete(RecordType.A, RecordType.AAAA);
* </code></pre>
*/
package codes.thischwa.cf.fluent;
@@ -0,0 +1,18 @@
package codes.thischwa.cf.model;
import lombok.Data;
/**
* Represents a base abstract entity class for modeling domain objects with a unique identifier.
*
* <p>This class provides a fundamental contract for entities by implementing the {@link
* ResponseEntity} interface. The primary attribute of this class is the `id` field, which serves as
* a unique identifier for all derived entities.
*
* <p>Commonly extended by other entity classes to maintain a consistent entity structure across the
* domain models. This encourages code reusability and consistency within the system.
*/
@Data
public class AbstractEntity implements ResponseEntity {
private String id;
}
@@ -0,0 +1,45 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Abstract base class for response models that contain multiple result entries.
*
* <p>This class is designed to handle API responses where multiple entities are part of the result
* set, such as paginated or batched data. It extends {@link AbstractResponse} to include additional
* attributes specific to multi-entity responses.
*
* <p>Attributes:
*
* <ul>
* <li>`resultInfo`: Provides metadata about the result set, such as pagination details like page
* number, total count, number of results per page, etc.
* <li>`result`: A list of entities representing the main body of the response. The type of
* entities in the result list is determined by the generic parameter {@code T}, which must
* extend {@link ResponseEntity}.
* </ul>
*
* <p>Subclasses can be created by specifying the entity type that the response should handle.
*
* @param <T> Represents the type of entities contained within the response. For this class, it is
* expected to be {@code ResponseEntity}.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public abstract class AbstractMultipleResponse<T extends ResponseEntity> extends AbstractResponse {
private ResultInfo resultInfo;
private List<T> result;
/**
* Default constructor for the {@code AbstractMultipleResponse} class.
*
* <p>Initializes a new instance of {@code AbstractMultipleResponse}, invoking the default
* constructor of the superclass {@code AbstractResponse}. This constructor is typically used for
* deserialization or subclass instantiation.
*/
AbstractMultipleResponse() {
super();
}
}
@@ -0,0 +1,28 @@
package codes.thischwa.cf.model;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.Data;
/**
* Abstract base class for API response models.
*
* <p>This class encapsulates common attributes used to represent the result of an API request. It
* can be extended to define more specific response structures.
*
* <p>Attributes:
*
* <ol>
* <li><b>success</b>: Indicates whether the API request was successful.
* <li><b>errors</b>: A list of error messages, if any, returned by the API.
* <li><b>messages</b>: A list of informational or status messages accompanying the response.
* </ol>
*
* <p>This structure is designed for consistency and ease of extending response models in
* applications that require uniform response structures.
*/
@Data
public abstract class AbstractResponse {
@JsonUnwrapped
private ResponseResultInfo responseResultInfo;
}
@@ -0,0 +1,24 @@
package codes.thischwa.cf.model;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Represents a base abstract response model for handling single response entities within an API
* response.
*
* <p>This class extends {@code AbstractResponse}, inheriting common response attributes such as
* success status, error messages, and informational messages. It introduces a single generic type
* parameter {@code <T>} which extends {@code ResponseEntity}, enforcing type consistency for the
* result attribute.
*
* <p>Primary Attribute: {@code result}: Represents the single entity result of the response. This
* is a generic type that ensures flexibility and adaptability for different entity models.
*
* @param <T> The type of the response entity that extends {@code ResponseEntity}.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public abstract class AbstractSingleResponse<T extends ResponseEntity> extends AbstractResponse {
private T result;
}
@@ -0,0 +1,39 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Represents a batch entry containing different types of operations on getRecord entities.
*
* <p>A BatchEntry groups together collections of operations (patches, posts, puts, and deletes)
* intended to be performed as part of a single batch process. Each operation corresponds to a specific
* type of action on DNS getRecord entities.
*
* <ul>
* <li><b>patches</b>: A list of {@link RecordEntity} objects representing partial updates to existing records.
* <li><b>posts</b>: A list of {@link RecordEntity} objects to be created as new DNS records.
* <li><b>puts</b>: A list of {@link RecordEntity} objects representing updates or replacements for existing records.
* <li><b>deletes</b>: A list of {@link RecordEntity} objects with name, type, and content to be removed.
* </ul>
*
* <p>This class is used as both a request body for batch operations and to represent the batch response.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class BatchEntry extends AbstractEntity {
List<RecordEntity> patches;
List<RecordEntity> posts;
List<RecordEntity> puts;
List<RecordEntity> deletes;
@Override
public String getId() {
return "";
}
}
@@ -0,0 +1,18 @@
package codes.thischwa.cf.model;
/**
* Represents a response that contains a single {@link BatchEntry} as the result.
*
* <p>This class is used for API responses where the primary result is a batch entry,
* which includes collections of operations such as patches, posts, puts, and deletes
* performed on DNS getRecord entities.
*
* <p>Extends {@code AbstractSingleResponse} with {@code BatchEntry} as the generic type,
* ensuring that the response result is a batch of operations.
*/
public class BatchResponse extends AbstractSingleResponse<BatchEntry> {
BatchResponse() {
super();
}
}
@@ -0,0 +1,83 @@
package codes.thischwa.cf.model;
import java.util.Map;
import lombok.Data;
/**
* Represents a request model for paginated data.
*
* <p>This class encapsulates the page number and the number of items per page for a paginated
* request, along with utility methods for constructing and retrieving pagination parameters.
*
* <p>Key functionalities:
*
* <ul>
* <li>Creating a {@code PagingRequest} instance with specific pagination values using the {@code
* of} method.
* <li>Creating a default {@code PagingRequest} with predefined pagination values.
* <li>Retrieving pagination parameters as a key-value map.
* <li>Generating a query string representation for the pagination parameters.
* </ul>
*/
@Data
public class PagingRequest {
/**
* Default page size for retrieving all records in a single request.
* Set to a very high value to effectively disable pagination when fetching all records.
*/
private static final int DEFAULT_ALL_RECORDS_PAGE_SIZE = 1000;
private int page;
private int perPage;
PagingRequest(int page, int perPage) {
this.page = page;
this.perPage = perPage;
}
/**
* Creates a new {@code PagingRequest} instance with the specified page number and items per page.
*
* @param page the page number to be requested
* @param perPage the number of items to be included per page
* @return a new {@code PagingRequest} instance with the provided parameters
*/
public static PagingRequest of(int page, int perPage) {
return new PagingRequest(page, perPage);
}
/**
* Creates a default {@code PagingRequest} instance with a page number set to 1 and a high number
* of items per page to accommodate large dataset requests and effectively retrieve all records.
*
* @return a default {@code PagingRequest} instance with predefined pagination parameters
*/
public static PagingRequest defaultPaging() {
return new PagingRequest(1, DEFAULT_ALL_RECORDS_PAGE_SIZE);
}
/**
* Retrieves the pagination parameters in a key-value map format.
*
* @return a map containing the pagination parameters, where the key "page" indicates the current
* page number and the key "perPage" indicates the number of items per page.
*/
public Map<String, String> getPagingParams() {
return Map.of("page", String.valueOf(page), "perPage", String.valueOf(perPage));
}
/**
* Appends a query string with pagination parameters (page and perPage) to the provided endpoint.
*
* @param endpoint the base URL or API endpoint to which the query string will be appended
* @return the complete URL with the appended query string for pagination
*/
public String addQueryString(String endpoint) {
return endpoint + queryString(endpoint.contains("?"));
}
private String queryString(boolean add) {
String qs = "page=" + page + "&per_page=" + perPage;
return add ? "&" + qs : "?" + qs;
}
}
@@ -0,0 +1,142 @@
package codes.thischwa.cf.model;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.Nullable;
/**
* Represents a DNS getRecord entity within a specific zone.
*
* <p>Attributes defined in this class include:
*
* <ul>
* <li>DNS getRecord type such as "A" or "CNAME".
* <li>Name of the DNS getRecord.
* <li>Content of the DNS getRecord, such as an IP address.
* <li>Flags indicating whether the getRecord is proxiable or proxied.
* <li>TTL (Time-To-Live) for the DNS getRecord.
* <li>A locked status to indicate the immutability of the getRecord.
* <li>Zone-specific metadata including zone ID and name.
* <li>Timestamps for creation and modification.
* </ul>
*
* <p>Provides a static factory method {@code build} for creating a DNS getRecord with specific
* attributes.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class RecordEntity extends AbstractEntity {
private String type;
private String name;
private String content;
private Boolean proxiable;
private Boolean proxied;
private Integer ttl;
private Boolean locked;
@Nullable
private String zoneId;
@Nullable
private String zoneName;
@Nullable
private LocalDateTime modifiedOn;
@Nullable
private LocalDateTime createdOn;
/**
* Initializes a new instance of the RecordEntity class and invokes the parent constructor from
* 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.
*/
public RecordEntity() {
super();
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
*
* @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) {
RecordEntity rec = new RecordEntity();
rec.name = name;
rec.type = type.getType();
rec.ttl = ttl;
rec.content = content;
return rec;
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified ID and content.
*
* @param id the unique identifier for the DNS getRecord
* @param content the content of the DNS getRecord, typically an IP address or other getRecord data
* @return a {@link RecordEntity} populated with the provided ID and content
*/
public static RecordEntity build(String id, String content) {
RecordEntity rec = new RecordEntity();
rec.setId(id);
rec.content = content;
return rec;
}
/**
* Builds and returns a {@link RecordEntity} instance with the specified attributes.
*
* @param id the unique identifier for the DNS getRecord
* @param name the name of the DNS getRecord
* @param type the type of the DNS getRecord, represented as a string (e.g., "A", "CNAME")
* @param ttl the time-to-live (TTL) value for the DNS getRecord
* @param content the content of the DNS getRecord, typically an IP address or other getRecord data
* @return a {@link RecordEntity} populated with the provided attributes
* @throws IllegalArgumentException if the type string is not a valid RecordType
*/
public static RecordEntity build(String id, String name, String type, Integer ttl, String content) {
RecordType recordType;
try {
recordType = RecordType.valueOf(type);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid getRecord type: " + type + ". Must be one of: "
+ java.util.Arrays.toString(RecordType.values()), e);
}
RecordEntity rec = new RecordEntity();
rec.setId(id);
rec.name = name;
rec.type = recordType.getType();
rec.ttl = ttl;
rec.content = content;
return rec;
}
/**
* Retrieves the short name (subdomain) of the DNS getRecord.
* If the name contains a dot ('.'), only the substring before the first dot is returned.
* This is useful for getting the subdomain part of a fully qualified domain name.
*
* @return the short name of the DNS getRecord (substring before the first dot),
* or the full name if no dot is present
*/
public String getSld() {
if (name == null) {
return null;
}
if (zoneName != null && name.endsWith(zoneName)) {
int zoneNameLength = zoneName.length();
int dotSeparatorLength = 1;
return name.substring(0, name.length() - zoneNameLength - dotSeparatorLength);
}
int firstDotPosition = name.indexOf('.');
if (firstDotPosition > 0) {
return name.substring(0, firstDotPosition);
}
return name;
}
}
@@ -0,0 +1,19 @@
package codes.thischwa.cf.model;
/**
* Represents the API response of the Cloudflare API containing multiple {@link RecordEntity}
* instances.
*/
public class RecordMultipleResponse extends AbstractMultipleResponse<RecordEntity> {
/**
* Constructs an instance of RecordMultipleResponse.
*
* <p>This class represents a response containing multiple DNS getRecord entities from the
* Cloudflare API. It inherits functionality from AbstractMultipleResponse to handle multiple
* records of type RecordEntity.
*/
public RecordMultipleResponse() {
super();
}
}
@@ -0,0 +1,20 @@
package codes.thischwa.cf.model;
/**
* Represents the API response of the Cloudflare API containing a single {@link RecordEntity}
* instance.
*/
public class RecordSingleResponse extends AbstractSingleResponse<RecordEntity> {
/**
* Constructs a new instance of the RecordSingleResponse class.
*
* <p>This constructor initializes the RecordSingleResponse object by invoking the superclass
* constructor. The RecordSingleResponse represents a specific API response structure that
* encapsulates a single DNS getRecord entity, providing mechanisms to interact with such data in the
* context of the Cloudflare API.
*/
public RecordSingleResponse() {
super();
}
}
@@ -0,0 +1,204 @@
package codes.thischwa.cf.model;
import lombok.Getter;
/**
* Enum representing various DNS getRecord types.
*
* <p>Each constant in this enum corresponds to a specific DNS getRecord type, such as "A", "AAAA",
* "CNAME", or "TXT". This enum provides a means to standardize the representation of these getRecord
* types throughout the application while allowing easy retrieval of their string representation.
*/
@Getter
public enum RecordType {
/**
* Represents the DNS A getRecord type.
*
* <p>The "A" getRecord type is used to map a domain name to an IPv4 address.
*/
A("A"),
/**
* Represents the DNS AAAA getRecord type.
*
* <p>The "AAAA" getRecord type maps a domain name to an IPv6 address.
*/
AAAA("AAAA"),
/**
* Represents the DNS CAA (Certificate Authority Authorization) getRecord type.
*
* <p>The "CAA" getRecord type is used to specify which certificate authorities (CAs) are allowed to
* issue SSL/TLS certificates for a domain.
*/
CAA("CAA"),
/**
* Represents the DNS CERT getRecord type.
*
* <p>The "CERT" getRecord type is used to store certificates and related certificate revocation
* lists or certificate authority data in DNS zones.
*/
CERT("CERT"),
/**
* Represents the DNS CNAME (Canonical Name) getRecord type.
*
* <p>The "CNAME" getRecord type is used to alias one domain name to another.
*/
CNAME("CNAME"),
/**
* Represents the DNSKEY getRecord type.
*
* <p>The "DNSKEY" getRecord type is used for storing public keys in DNS, as part of the DNS Security
* Extensions (DNSSEC). It helps in verifying the authenticity of DNS responses through digital
* signatures.
*/
DNSKEY("DNSKEY"),
/**
* Represents the DNS DS (Delegation Signer) getRecord type.
*
* <p>The "DS" getRecord type is used in the DNSSEC (Domain Name System Security Extensions)
* protocol. It contains a hash of a DNSKEY getRecord which is utilized in establishing a chain of
* trust from a parent zone to a child zone.
*/
DS("DS"),
/**
* Represents the DNS HTTPS (HTTP Service) getRecord type.
*
* <p>The "HTTPS" getRecord type is used to specify information about the HTTP/3 and related services
* offered by a domain.
*/
HTTPS("HTTPS"),
/**
* Represents the DNS LOC (Location) getRecord type.
*
* <p>The "LOC" getRecord type is used to store geographical location information for a domain,
* including latitude, longitude, altitude, and other details. It enables associating domains with
* physical locations.
*/
LOC("LOC"),
/**
* Represents the DNS MX (Mail Exchange) getRecord type.
*
* <p>The "MX" getRecord type is used to specify the mail servers responsible for receiving email
* messages on behalf of a domain.
*/
MX("MX"),
/**
* Represents the NAPTR getRecord type for DNS configurations.
*
* <p>This constant is used to specify NAPTR (Naming Authority Pointer) DNS records, which allow
* for service discovery through flexible DNS-based mechanisms. NAPTR records are commonly used in
* applications such as VoIP and ENUM (Telephone Number Mapping) for resolving information about
* available services.
*/
NAPTR("NAPTR"),
/**
* Represents the namespace or identifier `"NS"` within the domain model.
*
* <p>This variable typically designates elements related to name server operations or
* configurations in the context of the Cloudflare DNS system. It may be used as an identifier or
* constant throughout the application for operations involving name servers.
*/
NS("NS"),
/**
* Represents the "OPENPGPKEY" DNS getRecord type.
*
* <p>This constant is primarily used to identify DNS records of the type "OPENPGPKEY". It may be
* utilized within the system for operations such as creating, retrieving, updating, or managing
* DNS records specific to the "OPENPGPKEY" type.
*/
OPENPGPKEY("OPENPGPKEY"),
/**
* Represents a DNS getRecord type.
*
* <p>The `PTR` value specifically refers to a "pointer getRecord" in the DNS system, which is
* typically used for reverse DNS lookups.
*/
PTR("PTR"),
/**
* Represents the SMIMEA DNS getRecord type.
*
* <p>The SMIMEA resource getRecord is used to associate a user's certificate information for email
* message signing or encryption. This type of DNS getRecord is part of the DNS-based Authentication
* of Named Entities (DANE) protocol.
*
* <p>SMIMEA records provide a mechanism for utilizing certificates in email communication
* securely by publishing their information in DNS. They ensure integrity and authenticity of
* encrypted or signed email exchanges.
*
* <p>Key features include:
*
* <ul>
* <li>Use in Secure/Multipurpose Internet Mail Extensions (S/MIME)-based messaging.
* <li>Facilitating secure email communications by publishing certificates in DNS.
* <li>Enhancing security by eliminating dependency on third-party certificate authorities.
* </ul>
*/
SMIMEA("SMIMEA"),
/**
* Represents a service getRecord (SRV) type in the DNS configuration model.
*
* <p>This constant may be used to identify and work with SRV getRecord types in various DNS-related
* operations or integrations.
*/
SRV("SRV"),
/**
* Represents the DNS getRecord type "SSHFP" (SSH Fingerprint), used in DNS to store cryptographic
* fingerprints associated with SSH host keys.
*
* <p>This DNS getRecord type provides a mechanism for verifying the authenticity of an SSH server
* before initiating a connection, enhancing the security of SSH communications.
*/
SSHFP("SSHFP"),
/**
* Represents the Service Binding (SVCB) DNS getRecord type.
*
* <p>The SVCB getRecord is a DNS resource getRecord used to indicate alternative endpoints or specific
* configuration details for services. It is commonly applied in service discovery and
* protocol-specific configurations.
*/
SVCB("SVCB"),
/**
* Represents a constant for the DNS-based Authentication of Named Entities (DANE) TLSA getRecord
* type.
*
* <p>The TLSA getRecord is used to associate a TLS server certificate or public key with the domain
* name (e.g., via DNSSEC). It enables cryptographically secured connections by attaching
* certificate and key constraints to the specific domain.
*/
TLSA("TLSA"),
/**
* Represents the TXT DNS getRecord type.
*
* <p>The TXT DNS getRecord type is commonly used to store text-based information for various
* verification and configuration purposes, such as domain ownership verification or email
* authentication protocols (e.g., SPF, DKIM).
*/
TXT("TXT"),
/**
* Represents a Uniform Resource Identifier (URI).
*
* <p>This variable is used to define and manage URIs, which are string identifiers commonly
* utilized to specify the location of resources in various network-based systems.
*
* <p>Features and usage:
*
* <ul>
* <li>Provides a standard way of identifying resources via URI syntax.
* <li>Can encapsulate support for schemes such as HTTP, HTTPS, FTP, etc.
* </ul>
*
* <p>This variable serves as a critical component for resource identification and communication
* operations within the application.
*/
URI("URI");
private final String type;
RecordType(String type) {
this.type = type;
}
@Override
public String toString() {
return type;
}
}
@@ -0,0 +1,17 @@
package codes.thischwa.cf.model;
/**
* Represents a contract for entities that have a unique identifier.
*
* <p>This interface is primarily used as a common abstraction for domain objects that require a
* unique identifier, enabling type consistency and code reusability.
*/
public interface ResponseEntity {
/**
* Retrieves the unique identifier of the entity.
*
* @return the unique identifier as a String
*/
String getId();
}
@@ -0,0 +1,62 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
/**
* Represents the result of a response with metadata about its success and associated messages or
* errors.
*
* <p>This class provides a structure to capture the outcome of an operation, including:
* <ul>
* <li>Whether the operation was successful.
* <li>A list of error messages if the operation failed.
* <li>A list of informational or success messages.
* </ul>
* It can be used to standardize the response format in an application.
*/
@Data
public class ResponseResultInfo {
private boolean success;
private List<Error> errors;
private List<String> messages;
/**
* Represents an error with a specific code and message.
*
* <p>This class is used to encapsulate error information, including a numerical error code
* and a corresponding descriptive message. It is often used as part of a collection of errors
* to provide detailed diagnostics for failed operations or processes.
*/
@Data
public static class Error {
private int code;
private String message;
/**
* Constructs a new instance of the {@code Error} class with default values for its properties.
*
* <p>This no-argument constructor initializes an {@code Error} object without setting
* specific values for the error code or message. It is primarily used when an error needs to
* be created and set up later, or when default values are acceptable.
*/
public Error() {
}
/**
* Constructs an instance of the {@code Error} class with a specified error code and message.
*
* @param code the numerical code representing the error
* @param message the descriptive message providing details about the error
*/
public Error(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return String.format("%d: %s", code, message);
}
}
}
@@ -0,0 +1,26 @@
package codes.thischwa.cf.model;
/**
* Represents metadata for paginated results.
*
* <p>This class contains information about the current page, page size, total pages, and result
* counts, which can be utilized in managing and navigating through paginated data.
*
* @param page The current page number.
* @param perPage The number of results per page.
* @param totalPages The total number of pages available.
* @param count The number of results on the current page.
* @param totalCount The total number of results across all pages.
*/
public record ResultInfo(int page, int perPage, int totalPages, int count, int totalCount) {
/**
* Constructs a ResultInfo instance with the specified total count and default values for other
* fields. Just to use in tests!
*
* @param totalCount the total number of results across all pages
*/
public ResultInfo(int totalCount) {
this(0, 0, 0, 0, totalCount);
}
}
@@ -0,0 +1,51 @@
package codes.thischwa.cf.model;
import java.time.LocalDateTime;
import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Represents a DNS zone entity in the Cloudflare DNS system. <br>
*
* <p>This class encapsulates all relevant data and metadata associated with a zone, including but
* not limited to the following attributes:
*
* <ul>
* <li>Zone name.
* <li>Development mode status.
* <li>Active and original name servers linked to the zone.
* <li>Timestamps indicating when the zone was created, modified, or activated.
* <li>Current operational status of the zone (e.g., active, inactive).
* <li>Boolean flag indicating whether the zone is paused.
* <li>Zone type, representing the nature of the DNS zone (e.g., full, partial).
* </ul>
*
* <p>This class extends {@link AbstractEntity} to inherit basic entity properties and to provide a
* consistent interface across domain models.
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ZoneEntity extends AbstractEntity {
private String name;
private Integer developmentMode;
private Set<String> nameServers;
private Set<String> originalNameServers;
private LocalDateTime createdOn;
private LocalDateTime modifiedOn;
private LocalDateTime activatedOn;
private String status;
private Boolean paused;
private String type;
/**
* Default no-argument constructor for the ZoneEntity class.
*
* <p>This constructor initializes a new instance of the ZoneEntity class and invokes the parent
* constructor from the AbstractEntity class. The ZoneEntity class represents a domain model for a
* DNS zone within the Cloudflare DNS system.
*/
public ZoneEntity() {
super();
}
}
@@ -0,0 +1,17 @@
package codes.thischwa.cf.model;
/**
* Represents a response model that contains multiple {@link ZoneEntity} instances.
*/
public class ZoneMultipleResponse extends AbstractMultipleResponse<ZoneEntity> {
/**
* Constructs a new ZoneMultipleResponse object.
*
* <p>This constructor initializes an instance of ZoneMultipleResponse, which serves as a response
* model containing multiple {@link ZoneEntity} instances.
*/
public ZoneMultipleResponse() {
super();
}
}
@@ -0,0 +1,5 @@
/**
* The model of CloudflareDNS-java.
*/
package codes.thischwa.cf.model;
@@ -0,0 +1,5 @@
/**
* The base package of CloudflareDNS-java.
*/
package codes.thischwa.cf;
@@ -0,0 +1,157 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.RecordType;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.junit.jupiter.api.Test;
/**
* Unit tests for CfBasicHttpClient using mocked HTTP components.
* Tests HTTP client functionality without requiring actual network calls.
*/
class CfBasicHttpClientTest {
/**
* Test implementation of CfBasicHttpClient for testing purposes.
*/
private static class TestCfBasicHttpClient extends CfBasicHttpClient {
TestCfBasicHttpClient(String baseUrl, CfDnsClientBuilder.CfAuth auth) {
super(baseUrl, auth);
}
// Expose protected methods for testing
public <T extends codes.thischwa.cf.model.AbstractResponse> T testGetRequest(String endpoint, Class<T> responseType)
throws CloudflareApiException {
return getRequest(endpoint, responseType);
}
public <T extends codes.thischwa.cf.model.AbstractResponse> T testPostRequest(String endpoint, Object payload, Class<T> responseType)
throws CloudflareApiException {
return postRequest(endpoint, payload, responseType);
}
public <T extends codes.thischwa.cf.model.AbstractResponse> T testPutRequest(String endpoint, Object payload, Class<T> responseType)
throws CloudflareApiException {
return putRequest(endpoint, payload, responseType);
}
public <T extends codes.thischwa.cf.model.AbstractResponse> T testPatchRequest(String endpoint, Object payload, Class<T> responseType)
throws CloudflareApiException {
return patchRequest(endpoint, payload, responseType);
}
public <T extends codes.thischwa.cf.model.AbstractResponse> T testDeleteRequest(String endpoint, Class<T> responseType)
throws CloudflareApiException {
return deleteRequest(endpoint, responseType);
}
}
@Test
void testConstructor_WithApiToken() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
assertNotNull(client);
}
@Test
void testConstructor_WithEmailKey() {
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth("test@example.com", "test-key");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
assertNotNull(client);
}
@Test
void testApiException_unknowEndpoint() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
// Invalid JSON will cause a parsing error
CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> {
client.testGetRequest("/invalid-endpoint", RecordSingleResponse.class);
});
assertNotNull(exception);
assertTrue(exception.getMessage().contains("Unexpected error"));
Throwable cause = exception.getCause();
assertInstanceOf(CloudflareApiException.class, cause);
assertTrue(cause.getMessage().contains("No route for that URI"), "Expected error message: No route for that URI, but it was: " + cause.getMessage());
}
@Test
void testResultWrapper() throws Exception {
// Test the private ResultWrapper record indirectly through client behavior
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
// Any request will create and use a ResultWrapper internally
assertThrows(CloudflareApiException.class, () -> {
client.testGetRequest("/test", RecordSingleResponse.class);
});
}
@Test
void testAuthApplicationHeader_ApiToken() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("my-token");
HttpGet request = new HttpGet("https://api.cloudflare.com/test");
auth.applyAuth(request);
assertTrue(request.containsHeader("Authorization"));
assertEquals("Bearer my-token", request.getFirstHeader("Authorization").getValue());
}
@Test
void testAuthApplicationHeader_EmailKey() {
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth("test@example.com", "my-key");
HttpGet request = new HttpGet("https://api.cloudflare.com/test");
auth.applyAuth(request);
assertTrue(request.containsHeader("X-Auth-Email"));
assertTrue(request.containsHeader("X-Auth-Key"));
assertEquals("test@example.com", request.getFirstHeader("X-Auth-Email").getValue());
assertEquals("my-key", request.getFirstHeader("X-Auth-Key").getValue());
}
@Test
void testBaseUrlConstruction() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
// Test default base URL
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
assertNotNull(client);
}
@Test
void testObjectMapperInitialization() {
// ObjectMapper is initialized in constructor via JsonConf
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
// If ObjectMapper wasn't initialized, any request would fail with NullPointerException
assertThrows(CloudflareApiException.class, () -> {
client.testGetRequest("/test", RecordSingleResponse.class);
});
}
@Test
void testRequestPayloadSerialization() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
RecordEntity record = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
// The payload serialization happens inside postRequest
assertThrows(CloudflareApiException.class, () -> {
client.testPostRequest("/test", record, RecordSingleResponse.class);
});
}
}
@@ -0,0 +1,113 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Penetration-style tests with malicious and invalid inputs to ensure CfDnsClient behaves safely
* (throws appropriate exceptions, does not crash, and does not create unintended resources).
* <p>
* These tests will be skipped if API_EMAIL or API_KEY are not provided via environment variables.
*/
public class CfClientPenTest {
private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone
private static final String API_TOKEN = System.getenv("API_TOKEN");
@BeforeAll
static void checkEnv() {
assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping pen tests");
}
private CfDnsClient newClient() {
return new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
}
@Test
@DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException")
void testInvalidCredentialsShouldFail() {
// Use syntactically valid but wrong credentials
CfDnsClient badClient = new CfDnsClientBuilder().withEmailKeyAuth("invalid@example.com", UUID.randomUUID().toString()).build();
assertThrows(CloudflareApiException.class, badClient::zoneList);
}
@Test
@DisplayName("Malicious SLD inputs must not crash and should throw proper exception type")
void testMaliciousSldPatternsDoNotSucceed() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneGet(ZONE_STR);
List<String> syntacticallyInvalidSlds =
List.of("; rm -rf /", "| cat /etc/passwd", "`shutdown -h now`",
"<script>alert('x')</script>", "\"quoted\"");
List<String> syntacticallyValidOrNotAllowedFromCloudflare =
List.of(".", "..", "../..", "..%2F..%2F", "a".repeat(300), "$(reboot)", "emoji-🥷-忍者",
"doesnotexist", "abcdef12345", "unwahrscheinlich-" + System.currentTimeMillis());
for (String sld : syntacticallyInvalidSlds) {
assertThrows(IllegalArgumentException.class, () -> client.recordList(zone, sld),
"Should throw IllegalArgumentException for invalid SLD '" + sld + "'");
}
for (String sld : syntacticallyValidOrNotAllowedFromCloudflare) {
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, sld),
"Should throw CloudflareNotFoundException for valid but non-existing SLD '" + sld + "'");
}
}
@Test
@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);
String sld = "pentest-" + System.currentTimeMillis();
String fqdn = sld + "." + ZONE_STR;
// Ensure clean state and guarantee cleanup later
try {
client.recordDeleteTypeIfExists(zone, sld, RecordType.A, RecordType.AAAA, RecordType.CNAME);
} catch (Exception ignored) {
}
try {
// A getRecord with invalid IPv4
assertThrows(CloudflareApiException.class,
() -> client.recordCreate(zone, fqdn, 60, RecordType.A, "999.999.999.999"));
// AAAA getRecord with non-IP content
assertThrows(CloudflareApiException.class,
() -> client.recordCreate(zone, fqdn, 60, RecordType.AAAA, "not-an-ipv6"));
// TTL boundary checks
assertThrows(CloudflareApiException.class,
() -> client.recordCreate(zone, fqdn, -1, RecordType.A, "130.0.0.3"));
} finally {
// Best-effort cleanup in case anything slipped through
assertDoesNotThrow(
() -> client.recordDeleteTypeIfExists(zone, sld, RecordType.A, RecordType.AAAA,
RecordType.CNAME));
}
}
@Test
@DisplayName("recordDeleteTypeIfExists must be safe on non-existing SLD and types")
void testDeleteTypeIfExistsOnNonExistingIsSafe() throws Exception {
CfDnsClient client = newClient();
ZoneEntity zone = client.zoneGet(ZONE_STR);
String randomSld = "nonexist-" + System.currentTimeMillis();
// Should not throw even if nothing exists
assertDoesNotThrow(
() -> client.recordDeleteTypeIfExists(zone, randomSld, RecordType.A, RecordType.AAAA));
}
}
@@ -0,0 +1,528 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.PagingRequest;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@Slf4j
public class CfClientTest {
private static final String ZONE_STR = "mein-d-ns.de";
private static final String SLD_STR = "devsld";
private static final int TTL = 60;
private static final String API_TOKEN = System.getenv("API_TOKEN");
private final CfDnsClient client = new CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
@BeforeAll
static void checkEnv() {
assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping client tests");
}
@Test
void testUnknownSld() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
assertThrows(CloudflareNotFoundException.class, () -> client.recordList(zone, "unknown", RecordType.A));
}
@Test
void testAddHost() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
try {
// clean-up
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
RecordEntity record = RecordEntity.build(SLD_STR, RecordType.A, TTL, "127.0.0.1");
RecordEntity createdRecord = client.recordCreate(zone, record);
assertNotNull(createdRecord.getId());
assertEquals(SLD_STR, createdRecord.getSld());
assertEquals(RecordType.A.getType(), createdRecord.getType());
assertEquals(TTL, createdRecord.getTtl());
assertEquals("127.0.0.1", createdRecord.getContent());
assertNotNull(createdRecord.getCreatedOn());
List<RecordEntity> records = client.recordList(zone, SLD_STR, RecordType.A);
assertEquals(1, records.size());
RecordEntity fetchedRecord = records.get(0);
assertEquals(createdRecord.getId(), fetchedRecord.getId());
assertEquals(createdRecord.getContent(), fetchedRecord.getContent());
assertEquals(createdRecord.getType(), fetchedRecord.getType());
client.recordDelete(zone, createdRecord);
// test A and AAAA records for the same SLD
RecordEntity recordA = RecordEntity.build(SLD_STR, RecordType.A, TTL, "127.0.0.2");
RecordEntity recordAAAA = RecordEntity.build(SLD_STR, RecordType.AAAA, TTL, "2001:db8::1");
RecordEntity createdRecordA = client.recordCreate(zone, recordA);
RecordEntity createdRecordAAAA = client.recordCreate(zone, recordAAAA);
assertNotNull(createdRecordA.getId());
assertNotNull(createdRecordAAAA.getId());
assertEquals(SLD_STR, createdRecordA.getSld());
assertEquals(SLD_STR, createdRecordAAAA.getSld());
assertEquals(RecordType.A.getType(), createdRecordA.getType());
assertEquals(RecordType.AAAA.getType(), createdRecordAAAA.getType());
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zone, SLD_STR, RecordType.A, RecordType.AAAA));
} finally {
// cleanup in case of failures during test
try {
client.recordDeleteTypeIfExists(zone, SLD_STR, RecordType.A, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testZoneList() throws CloudflareApiException {
List<ZoneEntity> zones = client.zoneList();
assertNotNull(zones);
assertFalse(zones.isEmpty());
assertEquals(ZONE_STR, zones.get(0).getName());
zones = client.zoneList(PagingRequest.of(1, 100));
assertNotNull(zones);
assertFalse(zones.isEmpty());
assertEquals(ZONE_STR, zones.get(0).getName());
}
@Test
void testZoneListAnlFailedSldList() throws Exception {
List<ZoneEntity> zList = client.zoneList();
assertEquals(1, zList.size());
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zList.get(0), "not-existing"));
}
@Test
void testDns() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity z = client.zoneGet(ZONE_STR);
assertEquals("cf9d8b12f61423f280e0a3ea2a96d921", z.getId());
assertEquals("mein-d-ns.de", z.getName());
assertEquals("active", z.getStatus());
assertEquals(2, z.getNameServers().size());
assertTrue(z.getNameServers().contains("rafe.ns.cloudflare.com"));
assertTrue(z.getOriginalNameServers().size() >= 2);
assertTrue(z.getOriginalNameServers().contains("blair.ns.cloudflare.com"));
assertNotNull(z.getActivatedOn());
assertNotNull(z.getModifiedOn());
assertNotNull(z.getCreatedOn());
// for all other functions use a newly created SLD and ensure cleanup at the end
String randomSld = SLD_STR + "-" + System.currentTimeMillis();
String domain = randomSld + "." + ZONE_STR;
RecordEntity r;
RecordEntity createdRe1;
RecordEntity createdRe2;
try {
// ensure clean state
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A, RecordType.AAAA);
// create A getRecord using recordCreate with full domain
createdRe1 =
client.recordCreate(z, RecordEntity.build(domain, RecordType.A, TTL, "130.0.0.3"));
assertNotNull(createdRe1.getId());
assertEquals(randomSld + "." + ZONE_STR, createdRe1.getName());
assertEquals(randomSld, createdRe1.getSld());
assertEquals(RecordType.A.getType(), createdRe1.getType());
assertEquals(z.getId(), createdRe1.getZoneId());
assertEquals(TTL, createdRe1.getTtl());
assertEquals("130.0.0.3", createdRe1.getContent());
assertNotNull(createdRe1.getCreatedOn());
assertNotNull(createdRe1.getModifiedOn());
// verify recordList for A
List<RecordEntity> aRecords = client.recordList(z, randomSld, RecordType.A);
assertEquals(1, aRecords.size());
r = aRecords.get(0);
assertEquals("130.0.0.3", r.getContent());
// create AAAA getRecord using recordCreateSld
createdRe2 =
client.recordCreateSld(z, randomSld, TTL, RecordType.AAAA, "2a0a:4cc0:c0:2e4::1");
List<RecordEntity> aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA);
assertEquals(1, aaaaRecords.size());
r = aaaaRecords.get(0);
assertEquals(z.getId(), r.getZoneId());
assertEquals("2a0a:4cc0:c0:2e4::1", r.getContent());
assertEquals(RecordType.AAAA.getType(), r.getType());
// test recordList
List<RecordEntity> rList = client.recordList(z, randomSld);
assertEquals(2, rList.size());
for (RecordEntity re : rList) {
assertEquals(z.getId(), re.getZoneId());
if (Objects.equals(re.getType(), RecordType.A.getType())) {
assertEquals("130.0.0.3", re.getContent());
} else if (Objects.equals(re.getType(), RecordType.AAAA.getType())) {
assertEquals("2a0a:4cc0:c0:2e4::1", re.getContent());
} else {
fail(String.format("Unexpected getRecord type: %s", re.getType()));
}
}
// test recordList without SLD
List<RecordEntity> fullList = client.recordList(z);
assertTrue(fullList.size() >= 2);
assertTrue(fullList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(fullList.stream().anyMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(
fullList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType()) || re.getType().equals(RecordType.AAAA.getType())));
// test recordList with types without SLD
List<RecordEntity> aList = client.recordList(z, RecordType.A);
assertFalse(aList.isEmpty());
assertTrue(aList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(aList.stream().noneMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(aList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType())));
// test fluent api list
List<RecordEntity> fluentList = client.zone(ZONE_STR).list(RecordType.A);
assertFalse(fluentList.isEmpty());
assertTrue(fluentList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
// update AAAA getRecord
createdRe2.setContent("2a0a:4cc0:c0:2e4::2");
client.recordUpdate(z, createdRe2);
aaaaRecords = client.recordList(z, randomSld, RecordType.AAAA);
assertEquals(1, aaaaRecords.size());
r = aaaaRecords.get(0);
assertEquals("2a0a:4cc0:c0:2e4::2", r.getContent());
// verify A 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 getRecord and verify it's gone
assertTrue(client.recordDelete(z, createdRe2));
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(z, randomSld, RecordType.AAAA));
// 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));
} finally {
// cleanup in case of failures during test
try {
client.recordDeleteTypeIfExists(z, randomSld, RecordType.A, RecordType.AAAA);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testRecordEntityInvalidType() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> RecordEntity.build("id123", "example.com", "INVALID_TYPE", 60, "192.168.1.1"));
assertTrue(exception.getMessage().contains("Invalid getRecord type: INVALID_TYPE"));
assertTrue(exception.getMessage().contains("Must be one of:"));
}
private static final String IP_PREFIX = "130.0.0.";
private static final String UPDATED_IP_PREFIX = "130.1.0.";
@Test
void testBatch() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity zone = client.zoneGet(ZONE_STR);
List<String> sldNames = createSldNames();
List<RecordEntity> initialRecords = createInitialRecords(sldNames);
cleanupRecords(zone, sldNames);
try {
testBatchPost(zone, initialRecords, sldNames);
testBatchPatch(zone, sldNames);
testBatchDelete(zone, sldNames);
testBatchPut(zone, sldNames);
} finally {
cleanupRecords(zone, sldNames);
}
}
private List<String> createSldNames() {
return List.of(SLD_STR + "-1", SLD_STR + "-2", SLD_STR + "-3");
}
private List<RecordEntity> createInitialRecords(List<String> sldNames) {
List<RecordEntity> records = new ArrayList<>();
for (int i = 0; i < sldNames.size(); i++) {
records.add(RecordEntity.build(sldNames.get(i), RecordType.A, TTL, IP_PREFIX + (i + 1)));
}
return records;
}
private void cleanupRecords(ZoneEntity zone, List<String> sldNames) {
sldNames.forEach(sld -> {
try {
client.recordDeleteTypeIfExists(zone, sld, RecordType.A);
} catch (CloudflareApiException e) {
throw new RuntimeException(e);
}
});
}
private void testBatchPost(ZoneEntity zone, List<RecordEntity> records, List<String> sldNames) throws Exception {
// Use only first 2 records for POST
List<RecordEntity> postRecords = records.subList(0, 2);
BatchEntry batchEntry = client.recordBatch(zone, postRecords, null, null, null);
assertEquals(2, batchEntry.getPosts().size());
RecordEntity batchedRecord = batchEntry.getPosts().get(0);
assertValidBatchedRecord(batchedRecord, postRecords.get(0));
// Verify only the first 2 records
for (int i = 0; i < 2; i++) {
List<RecordEntity> records1 = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, records1.size());
assertEquals(IP_PREFIX + (i + 1), records1.get(0).getContent());
}
}
private void testBatchPatch(ZoneEntity zone, List<String> sldNames) throws Exception {
// Use first 2 records for PATCH
List<RecordEntity> patchRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
RecordEntity record = records.get(0);
record.setContent(UPDATED_IP_PREFIX + (i + 1));
patchRecords.add(record);
}
client.recordBatch(zone, null, null, patchRecords, null);
// Verify both records were updated
for (int i = 0; i < 2; i++) {
List<RecordEntity> updatedRecords = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, updatedRecords.size());
assertEquals(UPDATED_IP_PREFIX + (i + 1), updatedRecords.get(0).getContent());
}
}
private void testBatchDelete(ZoneEntity zone, List<String> sldNames) throws Exception {
// Delete first 2 records
List<RecordEntity> deleteRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
deleteRecords.add(records.get(0));
}
client.recordBatch(zone, null, null, null, deleteRecords);
// Verify both records are deleted
for (int i = 0; i < 2; i++) {
String sldName = sldNames.get(i);
assertThrows(CloudflareNotFoundException.class,
() -> client.recordList(zone, sldName, RecordType.A));
}
}
private void testBatchPut(ZoneEntity zone, List<String> sldNames) throws Exception {
// Create 2 new records first for PUT test
List<RecordEntity> newRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
RecordEntity record = RecordEntity.build(sldNames.get(i), RecordType.A, TTL, IP_PREFIX + (i + 1));
newRecords.add(record);
}
client.recordBatch(zone, newRecords, null, null, null);
// Now use PUT to replace them
List<RecordEntity> putRecords = new ArrayList<>();
for (int i = 0; i < 2; i++) {
List<RecordEntity> records = client.recordList(zone, sldNames.get(i), RecordType.A);
RecordEntity record = records.get(0);
record.setContent(UPDATED_IP_PREFIX + (i + 1));
putRecords.add(record);
}
client.recordBatch(zone, null, putRecords, null, null);
// Verify both records were updated
for (int i = 0; i < 2; i++) {
List<RecordEntity> updatedRecords = client.recordList(zone, sldNames.get(i), RecordType.A);
assertEquals(1, updatedRecords.size());
assertEquals(UPDATED_IP_PREFIX + (i + 1), updatedRecords.get(0).getContent());
}
}
private void assertValidBatchedRecord(RecordEntity batchedRecord, RecordEntity originalRecord) {
assertNotNull(batchedRecord.getId());
assertEquals(originalRecord.getSld(), batchedRecord.getSld());
assertEquals(originalRecord.getType(), batchedRecord.getType());
assertNotNull(batchedRecord.getCreatedOn());
}
@Test
void testFluentApi() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
String fluentSld = "fluent-" + System.currentTimeMillis();
try {
// Test fluent create
RecordEntity created = client.zone(ZONE_STR)
.getRecord(fluentSld)
.create(RecordType.A, "192.168.100.1", TTL);
assertNotNull(created.getId());
assertEquals(fluentSld, created.getSld());
assertEquals("192.168.100.1", created.getContent());
// Test fluent get
List<RecordEntity> records = client.zone(ZONE_STR)
.getRecord(fluentSld, RecordType.A)
.get();
assertEquals(1, records.size());
assertEquals("192.168.100.1", records.get(0).getContent());
// Test fluent update
RecordEntity updated = client.zone(ZONE_STR)
.getRecord(fluentSld, RecordType.A)
.update("192.168.100.2");
assertEquals("192.168.100.2", updated.getContent());
// Test fluent delete
client.zone(ZONE_STR)
.getRecord(fluentSld)
.delete(RecordType.A);
assertThrows(CloudflareNotFoundException.class,
() -> client.zone(ZONE_STR).getRecord(fluentSld, RecordType.A).get());
} finally {
try {
client.recordDeleteTypeIfExists(zone, fluentSld, RecordType.A);
} catch (Exception e) { /* ignore */ }
}
}
@Test
void testGroupRecordsByFqdn_withValidRecords() {
// Arrange
RecordEntity rec1 = RecordEntity.build("1", "example.com.", "A", 300, "192.168.1.1");
RecordEntity rec2 = RecordEntity.build("2", "example.com.", "AAAA", 300, "::1");
RecordEntity rec3 = RecordEntity.build("3", "sub.example.com.", "CNAME", 300, "example.com.");
List<RecordEntity> records = Arrays.asList(rec1, rec2, rec3);
// Act
Map<String, List<RecordEntity>> groupedRecords = CfDnsClient.groupRecordsByFqdn(records);
// Assert
assertNotNull(groupedRecords, "Resulting map should not be null.");
assertEquals(2, groupedRecords.size(), "The grouping should result in 2 FQDN keys.");
assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records.");
assertEquals(1, groupedRecords.get("sub.example.com.").size(), "The key 'sub.example.com.' should have 1 getRecord.");
}
@Test
void testGroupRecordsByFqdn_withMultipleRecordsSameFqdn() {
// Arrange
RecordEntity rec1 = RecordEntity.build("1", "example.com.", "A", 300, "192.168.1.1");
RecordEntity rec2 = RecordEntity.build("2", "example.com.", "AAAA", 300, "::1");
List<RecordEntity> records = Arrays.asList(rec1, rec2);
// Act
Map<String, List<RecordEntity>> groupedRecords = CfDnsClient.groupRecordsByFqdn(records);
// Assert
assertNotNull(groupedRecords, "Resulting map should not be null.");
assertEquals(1, groupedRecords.size(), "The grouping should result in 1 FQDN key.");
assertEquals(2, groupedRecords.get("example.com.").size(), "The key 'example.com.' should have 2 records.");
}
@Test
void testPaging() throws Exception {
ZoneEntity zone = client.zoneGet(ZONE_STR);
String pagingSld = "paging-" + System.currentTimeMillis();
try {
int existingCount = 0;
try {
List<RecordEntity> allRecords = client.recordList(zone);
existingCount = allRecords.size();
} catch (CloudflareApiException e) {
// ignore
}
// Calculate how many records we need to create to reach at least 12 total A records
// (to test paging with pageSize 5: page 1 = 5, page 2 = 5, page 3 = 2+)
int targetCount = 12;
int recordsToCreate = Math.max(0, targetCount - existingCount);
// Create additional A records if needed
List<RecordEntity> createdRecords = new ArrayList<>();
for (int i = 1; i <= recordsToCreate; i++) {
RecordEntity record = RecordEntity.build(pagingSld, RecordType.A, TTL, "127.0.0." + i);
RecordEntity created = client.recordCreate(zone, record);
createdRecords.add(created);
assertNotNull(created.getId());
}
// Test paging with page size of 5
PagingRequest page1Request = PagingRequest.of(1, 5);
List<RecordEntity> page1Records = client.recordList(zone, page1Request);
assertEquals(5, page1Records.size(), "First page should contain 5 records");
// 2nd page should also contain 5 records (if we have at least 12 total)
PagingRequest page2Request = PagingRequest.of(2, 5);
List<RecordEntity> page2Records = client.recordList(zone, page2Request);
assertEquals(5, page2Records.size(), "Second page should contain at least 5 records");
// 3rd page should contain 2 records
PagingRequest page3Request = PagingRequest.of(3, 5);
List<RecordEntity> page3Records = client.recordList(zone, page3Request);
assertEquals(2, page3Records.size(), "Third page should contain 2 records");
// Verify no overlap between pages
List<String> page1Ids = page1Records.stream().map(RecordEntity::getId).toList();
List<String> page2Ids = page2Records.stream().map(RecordEntity::getId).toList();
List<String> page3Ids = page3Records.stream().map(RecordEntity::getId).toList();
Set<String> generatedRecordIds = new HashSet<>(page1Ids);
generatedRecordIds.addAll(page2Ids);
generatedRecordIds.addAll(page3Ids);
assertEquals(createdRecords.size(), generatedRecordIds.size());
// Verify our created records are in the zone
List<RecordEntity> allRecords = client.recordList(zone);
Set<String> allRecordIds = allRecords.stream().map(RecordEntity::getId).collect(Collectors.toSet());
assertEquals(createdRecords.size(), allRecordIds.size());
assertTrue(allRecordIds.containsAll(generatedRecordIds));
} finally {
try {
client.recordDeleteTypeIfExists(zone, pagingSld, RecordType.A);
} catch (Exception e) { /* ignore */ }
}
}
}
@@ -0,0 +1,150 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
/**
* Unit tests for CfDnsClientBuilder and its authentication classes.
*/
class CfDnsClientBuilderTest {
@Test
void testBuildWithApiToken() {
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.build();
assertNotNull(client);
}
@Test
void testBuildWithEmailKey() {
CfDnsClient client = new CfDnsClientBuilder()
.withEmailKeyAuth("test@example.com", "test-key")
.build();
assertNotNull(client);
}
@Test
void testBuildWithCustomBaseUrl() {
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.withBaseUrl("https://custom-api.example.com")
.build();
assertNotNull(client);
}
@Test
void testBuildWithDefaultBaseUrl() {
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.build();
assertNotNull(client);
}
@Test
void testBuildWithEmptyResultThrowsException() {
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.withEmptyResultThrowsException(true)
.build();
assertNotNull(client);
}
@Test
void testBuildWithEmptyResultDoesNotThrowException() {
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.withEmptyResultThrowsException(false)
.build();
assertNotNull(client);
}
@Test
void testBuilderMethodChaining() {
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.withBaseUrl("https://custom-api.example.com")
.withEmptyResultThrowsException(true)
.build();
assertNotNull(client);
}
@Test
void testApiTokenAuth_BlankToken() {
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.ApiTokenAuth(" "));
}
@Test
void testEmailKeyAuth_ValidCredentials() {
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth(
"test@example.com",
"valid-key"
);
assertNotNull(auth);
}
@Test
void testEmailKeyAuth_BlankEmail() {
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.EmailKeyAuth(" ", "valid-key"));
}
@Test
void testEmailKeyAuth_BlankKey() {
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.EmailKeyAuth("test@example.com", " "));
}
@Test
void testEmailKeyAuth_BothBlank() {
assertThrows(IllegalArgumentException.class, () -> new CfDnsClientBuilder.EmailKeyAuth(" ", " "));
}
@Test
void testDefaultBaseUrl() {
assertEquals("https://api.cloudflare.com/client/v4", CfDnsClientBuilder.DEFAULT_BASEURL);
}
@Test
void testBuilderWithMultipleConfigurations() {
// Test switching auth methods in the same builder (last one wins)
CfDnsClient client = new CfDnsClientBuilder()
.withEmailKeyAuth("test@example.com", "old-key")
.withApiTokenAuth("new-token") // This should override the email/key auth
.build();
assertNotNull(client);
}
@Test
void testBuilderWithMultipleBaseUrls() {
// Test setting base URL multiple times (last one wins)
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.withBaseUrl("https://old-api.example.com")
.withBaseUrl("https://new-api.example.com") // This should override
.build();
assertNotNull(client);
}
@Test
void testEmptyResultThrowsExceptionToggle() {
// Test toggling the flag multiple times (last one wins)
CfDnsClient client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.withEmptyResultThrowsException(true)
.withEmptyResultThrowsException(false) // This should override
.build();
assertNotNull(client);
}
}
@@ -0,0 +1,399 @@
package codes.thischwa.cf;
import codes.thischwa.cf.fluent.ZoneOperations;
import codes.thischwa.cf.model.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for CfDnsClient using mocked HTTP client responses.
* Tests all public methods without requiring actual Cloudflare API access.
*/
@ExtendWith(MockitoExtension.class)
class CfDnsClientMockTest {
private CfDnsClient client;
@Mock
private CfBasicHttpClient mockHttpClient;
private static final String TEST_ZONE_ID = "zone123";
private static final String TEST_ZONE_NAME = "example.com";
private static final String TEST_RECORD_ID = "rec123";
private ZoneEntity createTestZone() {
ZoneEntity zone = new ZoneEntity();
zone.setId(TEST_ZONE_ID);
zone.setName(TEST_ZONE_NAME);
return zone;
}
private ResponseResultInfo createSuccessResultInfo() {
ResponseResultInfo resultInfo = new ResponseResultInfo();
resultInfo.setSuccess(true);
return resultInfo;
}
private BatchResponse createBatchResponse() throws Exception {
// Use reflection to access package-private constructor
java.lang.reflect.Constructor<BatchResponse> constructor = BatchResponse.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
private ResultInfo createResultInfo(int count) {
return new ResultInfo(count);
}
@BeforeEach
void setUp() throws Exception {
// Create a real client with API token auth
client = new CfDnsClientBuilder()
.withApiTokenAuth("test-token")
.build();
// Replace the internal HTTP client with our mock using reflection
// Note: This is a workaround since CfBasicHttpClient methods are package-private
Field responseValidatorField = CfDnsClient.class.getDeclaredField("responseValidator");
responseValidatorField.setAccessible(true);
responseValidatorField.set(client, new ResponseValidator(false));
}
@Test
void testGroupRecordsByFqdn() {
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity rec2 = RecordEntity.build("test.example.com", RecordType.AAAA, 300, "::1");
RecordEntity rec3 = RecordEntity.build("www.example.com", RecordType.A, 300, "1.2.3.5");
List<RecordEntity> records = List.of(rec1, rec2, rec3);
Map<String, List<RecordEntity>> grouped = CfDnsClient.groupRecordsByFqdn(records);
assertEquals(2, grouped.size());
assertEquals(2, grouped.get("test.example.com").size());
assertEquals(1, grouped.get("www.example.com").size());
}
@Test
void testGroupRecordsByFqdn_NullInput() {
Map<String, List<RecordEntity>> grouped = CfDnsClient.groupRecordsByFqdn(null);
assertNotNull(grouped);
assertTrue(grouped.isEmpty());
}
@Test
void testZoneList() throws Exception {
// Create mock zone response
ZoneMultipleResponse mockResponse = new ZoneMultipleResponse();
ZoneEntity zone1 = new ZoneEntity();
zone1.setId("zone1");
zone1.setName("example.com");
ZoneEntity zone2 = new ZoneEntity();
zone2.setId("zone2");
zone2.setName("test.com");
mockResponse.setResult(List.of(zone1, zone2));
ResponseResultInfo resultInfo = new ResponseResultInfo();
resultInfo.setSuccess(true);
mockResponse.setResponseResultInfo(resultInfo);
// Mock the HTTP client via spy
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(ZoneMultipleResponse.class));
List<ZoneEntity> zones = spyClient.zoneList();
assertNotNull(zones);
assertEquals(2, zones.size());
assertEquals("example.com", zones.get(0).getName());
assertEquals("test.com", zones.get(1).getName());
}
@Test
void testZoneGet() throws Exception {
// Create mock zone response
ZoneMultipleResponse mockResponse = new ZoneMultipleResponse();
ZoneEntity zone = new ZoneEntity();
zone.setId(TEST_ZONE_ID);
zone.setName(TEST_ZONE_NAME);
mockResponse.setResult(List.of(zone));
ResponseResultInfo resultInfo = new ResponseResultInfo();
resultInfo.setSuccess(true);
mockResponse.setResponseResultInfo(resultInfo);
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(ZoneMultipleResponse.class));
ZoneEntity result = spyClient.zoneGet(TEST_ZONE_NAME);
assertNotNull(result);
assertEquals(TEST_ZONE_ID, result.getId());
assertEquals(TEST_ZONE_NAME, result.getName());
}
@Test
void testZoneOperations() throws Exception {
// Create mock zone response
ZoneMultipleResponse mockResponse = new ZoneMultipleResponse();
ZoneEntity zone = new ZoneEntity();
zone.setId(TEST_ZONE_ID);
zone.setName(TEST_ZONE_NAME);
mockResponse.setResult(List.of(zone));
ResponseResultInfo resultInfo = new ResponseResultInfo();
resultInfo.setSuccess(true);
mockResponse.setResponseResultInfo(resultInfo);
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(ZoneMultipleResponse.class));
ZoneOperations ops = spyClient.zone(TEST_ZONE_NAME);
assertNotNull(ops);
}
@Test
void testRecordList_Zone() throws Exception {
ZoneEntity zone = new ZoneEntity();
zone.setId(TEST_ZONE_ID);
zone.setName(TEST_ZONE_NAME);
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
mockResponse.setResultInfo(createResultInfo(1));
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity rec2 = RecordEntity.build("www.example.com", RecordType.A, 300, "1.2.3.5");
mockResponse.setResult(List.of(rec1, rec2));
ResponseResultInfo resultInfo = new ResponseResultInfo();
resultInfo.setSuccess(true);
mockResponse.setResponseResultInfo(resultInfo);
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
List<RecordEntity> records = spyClient.recordList(zone);
assertNotNull(records);
assertEquals(2, records.size());
assertEquals("1.2.3.4", records.get(0).getContent());
}
@Test
void testRecordList_WithPaging() throws Exception {
ZoneEntity zone = createTestZone();
PagingRequest pagingRequest = PagingRequest.of(10, 1);
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
mockResponse.setResultInfo(createResultInfo(1));
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
mockResponse.setResult(List.of(rec1));
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
List<RecordEntity> records = spyClient.recordList(zone, pagingRequest);
assertNotNull(records);
assertEquals(1, records.size());
}
@Test
void testRecordList_BySld() throws Exception {
ZoneEntity zone = createTestZone();
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
mockResponse.setResultInfo(createResultInfo(1));
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
mockResponse.setResult(List.of(rec1));
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
List<RecordEntity> records = spyClient.recordList(zone, "test");
assertNotNull(records);
assertEquals(1, records.size());
assertEquals(TEST_ZONE_ID, records.get(0).getZoneId());
}
@Test
void testRecordList_BySldAndType() throws Exception {
ZoneEntity zone = createTestZone();
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
mockResponse.setResultInfo(createResultInfo(1));
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity rec2 = RecordEntity.build("test.example.com", RecordType.AAAA, 300, "::1");
mockResponse.setResult(List.of(rec1, rec2));
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
List<RecordEntity> records = spyClient.recordList(zone, "test", RecordType.A);
assertNotNull(records);
assertEquals(1, records.size());
assertEquals(RecordType.A, RecordType.valueOf(records.get(0).getType()));
}
@Test
void testRecordList_ByType() throws Exception {
ZoneEntity zone = createTestZone();
RecordMultipleResponse mockResponse = new RecordMultipleResponse();
mockResponse.setResultInfo(createResultInfo(1));
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity rec2 = RecordEntity.build("www.example.com", RecordType.AAAA, 300, "::1");
mockResponse.setResult(List.of(rec1, rec2));
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
List<RecordEntity> records = spyClient.recordList(zone, RecordType.A);
assertNotNull(records);
assertEquals(1, records.size());
assertEquals(RecordType.A, RecordType.valueOf(records.get(0).getType()));
}
@Test
void testRecordCreateSld() throws Exception {
ZoneEntity zone = createTestZone();
RecordSingleResponse mockResponse = new RecordSingleResponse();
RecordEntity createdRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
createdRecord.setId(TEST_RECORD_ID);
mockResponse.setResult(createdRecord);
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).postRequest(anyString(), any(), eq(RecordSingleResponse.class));
RecordEntity result = spyClient.recordCreate(zone, "test.example.com", 300, RecordType.A, "1.2.3.4");
assertNotNull(result);
assertEquals(TEST_RECORD_ID, result.getId());
assertEquals(TEST_ZONE_ID, result.getZoneId());
assertEquals("1.2.3.4", result.getContent());
}
@Test
void testRecordDelete() throws Exception {
ZoneEntity zone = createTestZone();
RecordSingleResponse mockResponse = new RecordSingleResponse();
RecordEntity deletedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
deletedRecord.setId(TEST_RECORD_ID);
mockResponse.setResult(deletedRecord);
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).deleteRequest(anyString(), eq(RecordSingleResponse.class));
boolean result = spyClient.recordDelete(zone, TEST_RECORD_ID);
assertTrue(result);
}
@Test
void testRecordUpdate() throws Exception {
ZoneEntity zone = createTestZone();
RecordEntity record = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
record.setId(TEST_RECORD_ID);
RecordSingleResponse mockResponse = new RecordSingleResponse();
RecordEntity updatedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.5");
updatedRecord.setId(TEST_RECORD_ID);
mockResponse.setResult(updatedRecord);
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).patchRequest(anyString(), any(), eq(RecordSingleResponse.class));
RecordEntity result = spyClient.recordUpdate(zone, record);
assertNotNull(result);
assertEquals(TEST_RECORD_ID, result.getId());
}
@Test
void testRecordDeleteTypeIfExists() throws Exception {
ZoneEntity zone = createTestZone();
RecordMultipleResponse listResponse = new RecordMultipleResponse();
listResponse.setResultInfo(createResultInfo(1));
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
rec1.setId(TEST_RECORD_ID);
listResponse.setResult(List.of(rec1));
listResponse.setResponseResultInfo(createSuccessResultInfo());
RecordSingleResponse deleteResponse = new RecordSingleResponse();
RecordEntity deletedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
deletedRecord.setId(TEST_RECORD_ID);
deleteResponse.setResult(deletedRecord);
deleteResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(listResponse).when(spyClient).getRequest(anyString(), eq(RecordMultipleResponse.class));
doReturn(deleteResponse).when(spyClient).deleteRequest(anyString(), eq(RecordSingleResponse.class));
// Should not throw exception
spyClient.recordDeleteTypeIfExists(zone, "test", RecordType.A);
verify(spyClient, times(1)).getRequest(anyString(), eq(RecordMultipleResponse.class));
verify(spyClient, times(1)).deleteRequest(anyString(), eq(RecordSingleResponse.class));
}
@Test
void testRecordDeleteTypeIfExists_NotFound() throws Exception {
ZoneEntity zone = createTestZone();
CfDnsClient spyClient = spy(client);
doThrow(new CloudflareNotFoundException("Not found")).when(spyClient).recordList(any(), anyString(), any());
// Should not throw exception when record doesn't exist
assertDoesNotThrow(() -> spyClient.recordDeleteTypeIfExists(zone, "test", RecordType.A));
}
@Test
void testRecordBatch() throws Exception {
ZoneEntity zone = createTestZone();
RecordEntity postRecord = RecordEntity.build("new.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity patchRecord = RecordEntity.build(TEST_RECORD_ID, "1.2.3.5");
RecordEntity deleteRecord = RecordEntity.build("old.example.com", RecordType.A, 300, "1.2.3.6");
deleteRecord.setId("rec999");
BatchResponse mockResponse = createBatchResponse();
BatchEntry resultEntry = new BatchEntry();
resultEntry.setPosts(List.of(postRecord));
resultEntry.setPatches(List.of(patchRecord));
mockResponse.setResult(resultEntry);
mockResponse.setResponseResultInfo(createSuccessResultInfo());
CfDnsClient spyClient = spy(client);
doReturn(mockResponse).when(spyClient).postRequest(anyString(), any(), eq(BatchResponse.class));
BatchEntry result = spyClient.recordBatch(zone,
List.of(postRecord),
null,
List.of(patchRecord),
List.of(deleteRecord));
assertNotNull(result);
assertNotNull(result.getPosts());
assertEquals(1, result.getPosts().size());
assertEquals(TEST_ZONE_ID, result.getPosts().get(0).getZoneId());
}
}
@@ -0,0 +1,58 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
public class CfRequestTest {
@Test
public void testBuildPathWithSingleVariable() {
String result = CfRequest.RECORD_CREATE.buildPath("zone123");
assertEquals("/zones/zone123/dns_records", result);
}
@Test
public void testBuildPathWithMultipleVariables() {
String result = CfRequest.RECORD_UPDATE.buildPath("zone123", "record456");
assertEquals("/zones/zone123/dns_records/record456", result);
}
@Test
public void testBuildPathWithoutVariables() {
String result = CfRequest.ZONE_LIST.buildPath();
assertEquals("/zones", result);
}
@Test
public void testBuildRecordInfoName() {
String result = CfRequest.RECORD_LIST_NAME.buildPath("zone123", "sub.domain.com");
assertEquals("/zones/zone123/dns_records?name=sub.domain.com", result);
}
@Test
public void testBuildRecordDelete() {
String result = CfRequest.RECORD_DELETE.buildPath("zone123", "record789");
assertEquals("/zones/zone123/dns_records/record789", result);
}
@Test
public void testBuildZoneInfo() {
String result = CfRequest.ZONE_INFO.buildPath("zone123");
assertEquals("/zones?name=zone123", result);
}
@Test
public void testBuildRecordInfo() {
String result = CfRequest.RECORD_LIST_NAME.buildPath("zone123", "sld.domain.com");
assertEquals("/zones/zone123/dns_records?name=sld.domain.com", result);
}
@Test
public void testBuildPathInvalidArguments() {
assertThrows(
IllegalArgumentException.class,
() -> CfRequest.RECORD_UPDATE.buildPath("zone123"));
}
}
@@ -0,0 +1,53 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.BatchResponse;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import java.io.InputStream;
import java.util.List;
import org.junit.jupiter.api.Test;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
public class ObjectMapperTest {
private final ObjectMapper mapper = JsonConf.initObjectMapper();
@Test
void testObjectMapper() {
ZoneMultipleResponse resp =
mapper.readValue(this.getClass().getResourceAsStream("/zone-list-response.json"),
ZoneMultipleResponse.class);
assertNotNull(resp.getResponseResultInfo());
}
@Test
void testErrorResponse() {
List<Class<? extends AbstractResponse>> respClasses =
List.of(RecordSingleResponse.class, RecordMultipleResponse.class, ZoneMultipleResponse.class, BatchResponse.class);
respClasses.forEach(this::assertErrorResponse);
}
private void assertErrorResponse(Class<? extends AbstractResponse> clazz) {
InputStream in = this.getClass().getResourceAsStream("/error-response.json");
try {
AbstractResponse resp = mapper.readValue(in, clazz);
assertNotNull(resp);
assertNotNull(resp.getResponseResultInfo());
ResponseResultInfo resultInfo = resp.getResponseResultInfo();
assertFalse(resultInfo.isSuccess());
assertEquals(1, resultInfo.getErrors().size());
assertEquals(81053, resultInfo.getErrors().get(0).getCode());
} catch (JacksonException e) {
fail("fail for " + clazz + ": " + e.getMessage());
}
}
}
@@ -0,0 +1,133 @@
package codes.thischwa.cf;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import codes.thischwa.cf.model.AbstractResponse;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.ResponseResultInfo;
import codes.thischwa.cf.model.ResultInfo;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ResponseValidatorTest {
@Mock
private AbstractResponse mockResponse;
@Mock
private ResponseResultInfo mockResultInfo;
@Mock
private RecordMultipleResponse mockMultipleResponse;
@Mock
private RecordSingleResponse mockSingleResponse;
@Mock
private RecordEntity mockRecordEntity;
private ResponseValidator validatorWithException;
private ResponseValidator validatorWithoutException;
@BeforeEach
void setUp() {
validatorWithException = new ResponseValidator(true);
validatorWithoutException = new ResponseValidator(false);
}
@Test
void validateSuccessfulResponse() {
when(mockResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
assertDoesNotThrow(() -> validatorWithException.validate(mockResponse, false));
}
@Test
void validateFailedResponse() {
List<ResponseResultInfo.Error> errors = new ArrayList<>();
ResponseResultInfo.Error error = new ResponseResultInfo.Error(1, "Fehler 1");
errors.add(error);
error = new ResponseResultInfo.Error(2, "Fehler 2");
errors.add(error);
when(mockResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(false);
when(mockResultInfo.getErrors()).thenReturn(errors);
CloudflareApiException exception = assertThrows(CloudflareApiException.class,
() -> validatorWithException.validate(mockResponse, false));
assertTrue(exception.getMessage().contains("Fehler 1"));
assertTrue(exception.getMessage().contains("Fehler 2"));
}
@Test
void validateSingleResultExpectedButMultipleFound() {
when(mockMultipleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockMultipleResponse.getResultInfo()).thenReturn(new ResultInfo(2));
CloudflareApiException exception = assertThrows(CloudflareApiException.class,
() -> validatorWithException.validate(mockMultipleResponse, true));
assertTrue(exception.getMessage().contains("Unexpected result count: 2"));
}
@Test
void validateEmptyResultWithExceptionEnabled() {
when(mockMultipleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockMultipleResponse.getResultInfo()).thenReturn(new ResultInfo(0));
assertThrows(CloudflareNotFoundException.class,
() -> validatorWithException.validate(mockMultipleResponse, false));
}
@Test
void validateEmptyResultWithExceptionDisabled() {
when(mockMultipleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockMultipleResponse.getResultInfo()).thenReturn(new ResultInfo(0));
assertDoesNotThrow(() -> validatorWithoutException.validate(mockMultipleResponse, true));
}
@Test
void validateSingleResultWithNullResultAndExceptionEnabled() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockSingleResponse.getResult()).thenReturn(null);
assertThrows(CloudflareNotFoundException.class,
() -> validatorWithException.validate(mockSingleResponse, false));
}
@Test
void validateSingleResultWithNullResultAndExceptionDisabled() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
// mockSingleResponse.getResult() returns null by default (no stubbing needed)
assertDoesNotThrow(() -> validatorWithoutException.validate(mockSingleResponse, false));
}
@Test
void validateSingleResultWithValidResult() {
when(mockSingleResponse.getResponseResultInfo()).thenReturn(mockResultInfo);
when(mockResultInfo.isSuccess()).thenReturn(true);
when(mockSingleResponse.getResult()).thenReturn(mockRecordEntity);
assertDoesNotThrow(() -> validatorWithException.validate(mockSingleResponse, false));
assertDoesNotThrow(() -> validatorWithoutException.validate(mockSingleResponse, false));
}
}
@@ -0,0 +1,248 @@
package codes.thischwa.cf.fluent;
import codes.thischwa.cf.CfDnsClient;
import codes.thischwa.cf.CloudflareApiException;
import codes.thischwa.cf.model.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for the Fluent API package (ZoneOperations and RecordOperations).
*/
class FluentApiTest {
private CfDnsClient mockClient;
private ZoneEntity testZone;
private static final String TEST_ZONE_ID = "zone123";
private static final String TEST_ZONE_NAME = "example.com";
private static final String TEST_SLD = "test";
private static final String TEST_RECORD_ID = "rec123";
@BeforeEach
void setUp() {
mockClient = mock(CfDnsClient.class);
testZone = new ZoneEntity();
testZone.setId(TEST_ZONE_ID);
testZone.setName(TEST_ZONE_NAME);
}
@Test
void testZoneOperations_GetRecord() throws CloudflareApiException {
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
RecordOperations recordOps = zoneOps.getRecord(TEST_SLD);
assertNotNull(recordOps);
assertInstanceOf(RecordOperationsImpl.class, recordOps);
}
@Test
void testZoneOperations_GetRecordWithTypes() throws CloudflareApiException {
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
RecordOperations recordOps = zoneOps.getRecord(TEST_SLD, RecordType.A, RecordType.AAAA);
assertNotNull(recordOps);
assertInstanceOf(RecordOperationsImpl.class, recordOps);
}
@Test
void testZoneOperations_List() throws CloudflareApiException {
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity rec2 = RecordEntity.build("www.example.com", RecordType.A, 300, "1.2.3.5");
List<RecordEntity> expectedRecords = List.of(rec1, rec2);
when(mockClient.recordList(eq(testZone), any(RecordType[].class)))
.thenReturn(expectedRecords);
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
List<RecordEntity> result = zoneOps.list();
assertNotNull(result);
assertEquals(2, result.size());
verify(mockClient, times(1)).recordList(eq(testZone), any(RecordType[].class));
}
@Test
void testZoneOperations_ListWithTypes() throws CloudflareApiException {
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
List<RecordEntity> expectedRecords = List.of(rec1);
when(mockClient.recordList(eq(testZone), any(RecordType[].class)))
.thenReturn(expectedRecords);
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
List<RecordEntity> result = zoneOps.list(RecordType.A);
assertNotNull(result);
assertEquals(1, result.size());
verify(mockClient, times(1)).recordList(eq(testZone), any(RecordType[].class));
}
@Test
void testRecordOperations_Get() throws CloudflareApiException {
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
List<RecordEntity> expectedRecords = List.of(rec1);
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
.thenReturn(expectedRecords);
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
List<RecordEntity> result = recordOps.get();
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("1.2.3.4", result.get(0).getContent());
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
}
@Test
void testRecordOperations_GetWithTypes() throws CloudflareApiException {
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
List<RecordEntity> expectedRecords = List.of(rec1);
RecordType[] types = {RecordType.A};
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), eq(types)))
.thenReturn(expectedRecords);
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, types);
List<RecordEntity> result = recordOps.get();
assertNotNull(result);
assertEquals(1, result.size());
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), eq(types));
}
@Test
void testRecordOperations_Create() throws CloudflareApiException {
RecordEntity createdRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
createdRecord.setId(TEST_RECORD_ID);
when(mockClient.recordCreateSld(eq(testZone), eq(TEST_SLD), eq(300), eq(RecordType.A), eq("1.2.3.4")))
.thenReturn(createdRecord);
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
RecordEntity result = recordOps.create(RecordType.A, "1.2.3.4", 300);
assertNotNull(result);
assertEquals(TEST_RECORD_ID, result.getId());
assertEquals("1.2.3.4", result.getContent());
verify(mockClient, times(1)).recordCreateSld(eq(testZone), eq(TEST_SLD), eq(300), eq(RecordType.A), eq("1.2.3.4"));
}
@Test
void testRecordOperations_Update_Success() throws CloudflareApiException {
RecordEntity existingRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
existingRecord.setId(TEST_RECORD_ID);
RecordEntity updatedRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.5");
updatedRecord.setId(TEST_RECORD_ID);
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
.thenReturn(List.of(existingRecord));
when(mockClient.recordUpdate(eq(testZone), any(RecordEntity.class)))
.thenReturn(updatedRecord);
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
RecordEntity result = recordOps.update("1.2.3.5");
assertNotNull(result);
assertEquals("1.2.3.5", result.getContent());
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
verify(mockClient, times(1)).recordUpdate(eq(testZone), any(RecordEntity.class));
}
@Test
void testRecordOperations_Update_NoRecordsFound() throws CloudflareApiException {
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
.thenReturn(List.of());
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> {
recordOps.update("1.2.3.5");
});
assertTrue(exception.getMessage().contains("No recs found"));
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
verify(mockClient, never()).recordUpdate(any(), any());
}
@Test
void testRecordOperations_Update_MultipleRecordsFound() throws CloudflareApiException {
RecordEntity rec1 = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
RecordEntity rec2 = RecordEntity.build("test.example.com", RecordType.AAAA, 300, "::1");
when(mockClient.recordList(eq(testZone), eq(TEST_SLD), any()))
.thenReturn(List.of(rec1, rec2));
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
CloudflareApiException exception = assertThrows(CloudflareApiException.class, () -> {
recordOps.update("1.2.3.5");
});
assertTrue(exception.getMessage().contains("Multiple recs found"));
verify(mockClient, times(1)).recordList(eq(testZone), eq(TEST_SLD), any());
verify(mockClient, never()).recordUpdate(any(), any());
}
@Test
void testRecordOperations_Delete() throws CloudflareApiException {
doNothing().when(mockClient).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
recordOps.delete(RecordType.A, RecordType.AAAA);
verify(mockClient, times(1)).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
}
@Test
void testRecordOperations_DeleteSingleType() throws CloudflareApiException {
doNothing().when(mockClient).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
RecordOperations recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, null);
recordOps.delete(RecordType.A);
verify(mockClient, times(1)).recordDeleteTypeIfExists(eq(testZone), eq(TEST_SLD), any(RecordType[].class));
}
@Test
void testFluentApiChaining() throws CloudflareApiException {
// Test that fluent API chaining works correctly
RecordEntity createdRecord = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
createdRecord.setId(TEST_RECORD_ID);
when(mockClient.recordCreateSld(eq(testZone), eq(TEST_SLD), eq(300), eq(RecordType.A), eq("1.2.3.4")))
.thenReturn(createdRecord);
ZoneOperations zoneOps = new ZoneOperationsImpl(mockClient, testZone);
RecordEntity result = zoneOps.getRecord(TEST_SLD).create(RecordType.A, "1.2.3.4", 300);
assertNotNull(result);
assertEquals(TEST_RECORD_ID, result.getId());
}
@Test
void testConstructorFields() {
// Test that constructor properly initializes fields
RecordType[] types = {RecordType.A, RecordType.AAAA};
RecordOperationsImpl recordOps = new RecordOperationsImpl(mockClient, testZone, TEST_SLD, types);
assertNotNull(recordOps);
}
@Test
void testZoneOperationsImplConstructor() {
ZoneOperationsImpl zoneOps = new ZoneOperationsImpl(mockClient, testZone);
assertNotNull(zoneOps);
}
}
@@ -0,0 +1,47 @@
package codes.thischwa.cf.model;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class BatchEntryTest {
@Test
void testBatchEntry() {
BatchEntry entry = new BatchEntry();
assertEquals("", entry.getId());
List<RecordEntity> patches = new ArrayList<>();
List<RecordEntity> posts = new ArrayList<>();
List<RecordEntity> puts = new ArrayList<>();
List<RecordEntity> deletes = new ArrayList<>();
entry.setPatches(patches);
entry.setPosts(posts);
entry.setPuts(puts);
entry.setDeletes(deletes);
assertSame(patches, entry.getPatches());
assertSame(posts, entry.getPosts());
assertSame(puts, entry.getPuts());
assertSame(deletes, entry.getDeletes());
}
@Test
void testLombokMethods() {
BatchEntry entry1 = new BatchEntry();
BatchEntry entry2 = new BatchEntry();
assertEquals(entry1, entry2);
assertEquals(entry1.hashCode(), entry2.hashCode());
List<RecordEntity> patches = List.of(new RecordEntity());
entry1.setPatches(patches);
assertNotEquals(entry1, entry2);
entry2.setPatches(patches);
assertEquals(entry1, entry2);
assertTrue(entry1.toString().contains("patches="));
}
}
@@ -0,0 +1,43 @@
package codes.thischwa.cf.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class PagingRequestTest {
@Test
void testBuildPath() {
String result = PagingRequest.defaultPaging().addQueryString("/zones");
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&per_page=100", result);
}
@Test
void testGetPagingParams() {
PagingRequest request = PagingRequest.of(2, 50);
java.util.Map<String, String> params = request.getPagingParams();
assertEquals("2", params.get("page"));
assertEquals("50", params.get("perPage"));
}
@Test
void testLombokMethods() {
PagingRequest req1 = PagingRequest.of(1, 10);
PagingRequest req2 = PagingRequest.of(1, 10);
assertEquals(req1, req2);
assertEquals(req1.hashCode(), req2.hashCode());
assertTrue(req1.toString().contains("page=1"));
req1.setPage(5);
assertEquals(5, req1.getPage());
req1.setPerPage(20);
assertEquals(20, req1.getPerPage());
}
}
@@ -0,0 +1,70 @@
package codes.thischwa.cf.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class RecordEntityTest {
@Test
void testBuildWithAttributes() {
RecordEntity rec = RecordEntity.build("example.com", RecordType.A, 120, "1.2.3.4");
assertEquals("example.com", rec.getName());
assertEquals("A", rec.getType());
assertEquals(120, rec.getTtl());
assertEquals("1.2.3.4", rec.getContent());
}
@Test
void testBuildWithIdAndContent() {
RecordEntity rec = RecordEntity.build("id-123", "1.2.3.4");
assertEquals("id-123", rec.getId());
assertEquals("1.2.3.4", rec.getContent());
}
@Test
void testBuildWithIdAndAttributes() {
RecordEntity rec = RecordEntity.build("id-123", "example.com", "A", 120, "1.2.3.4");
assertEquals("id-123", rec.getId());
assertEquals("example.com", rec.getName());
assertEquals("A", rec.getType());
assertEquals(120, rec.getTtl());
assertEquals("1.2.3.4", rec.getContent());
}
@Test
void testBuildWithInvalidType() {
assertThrows(IllegalArgumentException.class, () ->
RecordEntity.build("id-123", "example.com", "INVALID", 120, "1.2.3.4")
);
}
@Test
void testGetSld() {
RecordEntity rec = new RecordEntity();
assertNull(rec.getSld());
rec.setName("sub.example.com");
assertEquals("sub", rec.getSld());
rec.setName("example.com");
assertEquals("example", rec.getSld());
rec.setName("host");
assertEquals("host", rec.getSld());
rec.setName(".dotstart");
assertEquals(".dotstart", rec.getSld());
}
@Test
void testGetSldWithZoneName() {
RecordEntity rec = new RecordEntity();
rec.setName("sub.example.com");
rec.setZoneName("example.com");
assertEquals("sub", rec.getSld());
rec.setName("my.sub.example.com");
rec.setZoneName("example.com");
assertEquals("my.sub", rec.getSld());
}
}
@@ -0,0 +1,40 @@
package codes.thischwa.cf.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class RecordTypeTest {
@Test
void testGetType() {
assertEquals("A", RecordType.A.getType());
assertEquals("AAAA", RecordType.AAAA.getType());
assertEquals("CNAME", RecordType.CNAME.getType());
assertEquals("TXT", RecordType.TXT.getType());
assertEquals("SRV", RecordType.SRV.getType());
assertEquals("LOC", RecordType.LOC.getType());
assertEquals("MX", RecordType.MX.getType());
assertEquals("NS", RecordType.NS.getType());
assertEquals("CAA", RecordType.CAA.getType());
assertEquals("CERT", RecordType.CERT.getType());
assertEquals("DNSKEY", RecordType.DNSKEY.getType());
assertEquals("DS", RecordType.DS.getType());
assertEquals("NAPTR", RecordType.NAPTR.getType());
assertEquals("SMIMEA", RecordType.SMIMEA.getType());
assertEquals("SSHFP", RecordType.SSHFP.getType());
assertEquals("TLSA", RecordType.TLSA.getType());
assertEquals("URI", RecordType.URI.getType());
}
@Test
void testToString() {
assertEquals("A", RecordType.A.toString());
assertEquals("CNAME", RecordType.CNAME.toString());
}
@Test
void testValueOf() {
assertEquals(RecordType.A, RecordType.valueOf("A"));
assertEquals(RecordType.CNAME, RecordType.valueOf("CNAME"));
}
}
@@ -0,0 +1,41 @@
package codes.thischwa.cf.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.time.LocalDateTime;
import java.util.Set;
import org.junit.jupiter.api.Test;
public class ZoneEntityTest {
@Test
void testZoneEntity() {
ZoneEntity zone = new ZoneEntity();
zone.setId("zone-id");
zone.setName("example.com");
zone.setDevelopmentMode(7200);
Set<String> ns = Set.of("ns1.cloudflare.com", "ns2.cloudflare.com");
zone.setNameServers(ns);
zone.setOriginalNameServers(ns);
LocalDateTime now = LocalDateTime.now();
zone.setCreatedOn(now);
zone.setModifiedOn(now);
zone.setActivatedOn(now);
zone.setStatus("active");
zone.setPaused(false);
zone.setType("full");
assertEquals("zone-id", zone.getId());
assertEquals("example.com", zone.getName());
assertEquals(7200, zone.getDevelopmentMode());
assertEquals(ns, zone.getNameServers());
assertEquals(ns, zone.getOriginalNameServers());
assertEquals(now, zone.getCreatedOn());
assertEquals(now, zone.getModifiedOn());
assertEquals(now, zone.getActivatedOn());
assertEquals("active", zone.getStatus());
assertFalse(zone.getPaused());
assertEquals("full", zone.getType());
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"result": null,
"success": false,
"errors": [
{
"code": 81053,
"message": "An A, AAAA, or CNAME record with that host already exists. For more details, refer to \u003chttps://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/\u003e."
}
],
"messages": []
}
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="current"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache.hc.client5.http" level="info"/>
<logger name="codes.thischwa.cf.CfBasicHttpClient" level="trace"/>
<root level="debug">
<appender-ref ref="current"/>
</root>
</configuration>
+174
View File
@@ -0,0 +1,174 @@
{
"result": [
{
"id": "0a83dd6e7f8c46039f2517bbded8115e",
"name": "mein-d-ns.de",
"status": "active",
"paused": false,
"type": "full",
"development_mode": -230181,
"name_servers": [
"blair.ns.cloudflare.com",
"sergi.ns.cloudflare.com"
],
"original_name_servers": [
"c.ns14.net",
"b.ns14.net",
"a.ns14.net",
"d.ns14.net"
],
"original_registrar": null,
"original_dnshost": null,
"modified_on": "2025-01-20T09:06:52.122538Z",
"created_on": "2025-01-20T08:56:34.736214Z",
"activated_on": "2025-01-20T09:06:52.122538Z",
"meta": {
"step": 2,
"custom_certificate_quota": 0,
"page_rule_quota": 3,
"phishing_detected": false
},
"owner": {
"id": null,
"type": "user",
"email": null
},
"account": {
"id": "563335e3d73549dbbc0620f4ce82d527",
"name": "myAccount"
},
"tenant": {
"id": null,
"name": null
},
"tenant_unit": {
"id": null
},
"permissions": [
"#image:read",
"#image:edit",
"#worker:edit",
"#analytics:read",
"#ssl:edit",
"#zone_settings:read",
"#organization:edit",
"#waf:read",
"#waf:edit",
"#logs:read",
"#member:read",
"#worker:read",
"#blocks:read",
"#blocks:edit",
"#access:read",
"#access:edit",
"#billing:read",
"#teams:read",
"#organization:read",
"#logs:edit",
"#zaraz:publish",
"#zone_settings:edit",
"#fbm:read",
"#subscription:read",
"#lb:read",
"#waitingroom:edit",
"#ssl:read",
"#subscription:edit",
"#dns_records:read",
"#dns_records:edit",
"#api_gateway:read",
"#api_gateway:edit",
"#cds_compute_account:edit",
"#ces_submissions:read",
"#fbm:edit",
"#healthchecks:edit",
"#stream:edit",
"#lb:edit",
"#healthchecks:read",
"#billing:edit",
"#member:edit",
"#cfone_read",
"#cfone_edit",
"#integration:read",
"#zaraz:edit",
"#magic:read",
"#magic:edit",
"#zaraz:read",
"#stream:read",
"#integration:install",
"#dash_sso:edit",
"#zone:read",
"#http_applications:read",
"#http_applications:edit",
"#ces_analytics:read",
"#resilience:edit",
"#teams:edit",
"#r2_bucket_warehouse:read",
"#r2_bucket_warehouse:edit",
"#query_cache:read",
"#query_cache:edit",
"#dex:read",
"#zone_versioning:read",
"#zone_versioning:edit",
"#cds:read",
"#ces_search:action",
"#ces_search:read",
"#ces_search:preview",
"#ces_search:raw",
"#ces_search:trace",
"#fbm_acc:edit",
"#teams:pii",
"#r2_bucket_item:read",
"#r2_bucket_item:edit",
"#integration:edit",
"#resilience:read",
"#zone:edit",
"#dash_sso:read",
"#legal:read",
"#ces_integration:edit",
"#ces_integration:read",
"#cache_purge:edit",
"#vectorize:read",
"#vectorize:edit",
"#auditlogs:read",
"#teams:report",
"#ces_settings:edit",
"#ces_settings:read",
"#waitingroom:read",
"#web3:read",
"#web3:edit",
"#dex:edit",
"#cds_compute_account:read",
"#cds:edit",
"#r2_bucket:read",
"#r2_bucket:edit",
"#ces_phishguard:read",
"#page_shield:read",
"#page_shield:edit",
"#legal:edit",
"#app:edit"
],
"plan": {
"id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Free Website",
"price": 0,
"currency": "USD",
"frequency": "",
"is_subscribed": false,
"can_subscribe": false,
"legacy_id": "free",
"legacy_discount": false,
"externally_managed": false
}
}
],
"result_info": {
"page": 1,
"per_page": 20,
"total_pages": 1,
"count": 1,
"total_count": 1
},
"success": true,
"errors": [],
"messages": []
}