Add unit tests for CfDnsClient, Fluent API, and CfDnsClientBuilder. Minor error message refinement in CfBasicHttpClient.

This commit is contained in:
2026-03-10 19:11:32 +01:00
parent 66a5d48927
commit 4e7b3b9bdb
6 changed files with 958 additions and 5 deletions
@@ -33,6 +33,9 @@ abstract class CfBasicHttpClient {
private final CfDnsClientBuilder.CfAuth auth;
private final ObjectMapper objectMapper;
private record ResultWrapper(int statusCode, String responseBody) {
}
/**
* Creates a new Cloudflare HTTP client with the specified base URL and authentication.
*
@@ -97,7 +100,7 @@ abstract class CfBasicHttpClient {
log.error("JSON parsing error for request to {}", logUri, e);
throw new CloudflareApiException("Error processing JSON response", e);
} catch (Exception e) {
throw new CloudflareApiException("Server error!", e);
throw new CloudflareApiException("Unexpected error!", e);
}
}
@@ -187,7 +190,4 @@ abstract class CfBasicHttpClient {
private String buildUrl(String endpoint) {
return baseUrl + endpoint;
}
private record ResultWrapper(int statusCode, String responseBody) {
}
}
@@ -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("API error: 10404: No route for that URI"));
}
@Test
void testResultWrapper() throws Exception {
// Test the private ResultWrapper record indirectly through client behavior
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
// Any request will create and use a ResultWrapper internally
assertThrows(CloudflareApiException.class, () -> {
client.testGetRequest("/test", RecordSingleResponse.class);
});
}
@Test
void testAuthApplicationHeader_ApiToken() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("my-token");
HttpGet request = new HttpGet("https://api.cloudflare.com/test");
auth.applyAuth(request);
assertTrue(request.containsHeader("Authorization"));
assertEquals("Bearer my-token", request.getFirstHeader("Authorization").getValue());
}
@Test
void testAuthApplicationHeader_EmailKey() {
CfDnsClientBuilder.EmailKeyAuth auth = new CfDnsClientBuilder.EmailKeyAuth("test@example.com", "my-key");
HttpGet request = new HttpGet("https://api.cloudflare.com/test");
auth.applyAuth(request);
assertTrue(request.containsHeader("X-Auth-Email"));
assertTrue(request.containsHeader("X-Auth-Key"));
assertEquals("test@example.com", request.getFirstHeader("X-Auth-Email").getValue());
assertEquals("my-key", request.getFirstHeader("X-Auth-Key").getValue());
}
@Test
void testBaseUrlConstruction() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
// Test default base URL
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
assertNotNull(client);
}
@Test
void testObjectMapperInitialization() {
// ObjectMapper is initialized in constructor via JsonConf
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
// If ObjectMapper wasn't initialized, any request would fail with NullPointerException
assertThrows(CloudflareApiException.class, () -> {
client.testGetRequest("/test", RecordSingleResponse.class);
});
}
@Test
void testRequestPayloadSerialization() {
CfDnsClientBuilder.ApiTokenAuth auth = new CfDnsClientBuilder.ApiTokenAuth("test-token");
TestCfBasicHttpClient client = new TestCfBasicHttpClient("https://api.cloudflare.com", auth);
RecordEntity record = RecordEntity.build("test.example.com", RecordType.A, 300, "1.2.3.4");
// The payload serialization happens inside postRequest
assertThrows(CloudflareApiException.class, () -> {
client.testPostRequest("/test", record, RecordSingleResponse.class);
});
}
}
@@ -198,7 +198,6 @@ public class CfClientTest {
// test recordList with types without SLD
List<RecordEntity> aList = client.recordList(z, RecordType.A);
assertFalse(aList.isEmpty());
assertTrue(aList.size() >= 1);
assertTrue(aList.stream().anyMatch(re -> re.getId().equals(createdRe1.getId())));
assertTrue(aList.stream().noneMatch(re -> re.getId().equals(createdRe2.getId())));
assertTrue(aList.stream().allMatch(re -> re.getType().equals(RecordType.A.getType())));
@@ -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,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);
}
}