From f1dcd00a9daf1ed643ccebe774ac17d88ea641c3 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 1 Apr 2026 09:44:36 +0530 Subject: [PATCH] add unit tests --- .../dns/powerdns/PowerDnsClientTest.java | 279 +++++- .../dns/powerdns/PowerDnsProviderTest.java | 391 ++++++++ .../com/cloud/event/ActionEventUtils.java | 4 - .../dns/DnsVmLifecycleListener.java | 196 ---- .../dns/DnsProviderManagerImplTest.java | 892 ++++++++++++++++++ .../cloudstack/dns/DnsProviderUtilTest.java | 91 ++ .../dns/dao/DnsServerDaoImplTest.java | 117 +++ .../dns/dao/DnsZoneDaoImplTest.java | 111 +++ 8 files changed, 1878 insertions(+), 203 deletions(-) create mode 100644 plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java delete mode 100644 server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java create mode 100644 server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java create mode 100644 server/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java create mode 100644 server/src/test/java/org/apache/cloudstack/dns/dao/DnsServerDaoImplTest.java create mode 100644 server/src/test/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImplTest.java diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java index d0a73e48d87..e79b391affd 100644 --- a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsClientTest.java @@ -18,13 +18,65 @@ package org.apache.cloudstack.dns.powerdns; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.apache.cloudstack.dns.exception.DnsOperationException; +import org.apache.cloudstack.dns.exception.DnsAuthenticationException; +import org.apache.cloudstack.dns.exception.DnsConflictException; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Before; import org.junit.Test; -import org.mockito.InjectMocks; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.test.util.ReflectionTestUtils; +import com.fasterxml.jackson.databind.JsonNode; + +@RunWith(MockitoJUnitRunner.class) public class PowerDnsClientTest { - @InjectMocks - PowerDnsClient client = new PowerDnsClient(); + + PowerDnsClient client; + CloseableHttpClient httpClientMock; + + @Before + public void setUp() { + client = new PowerDnsClient(); + httpClientMock = mock(CloseableHttpClient.class); + ReflectionTestUtils.setField(client, "httpClient", httpClientMock); + } + + private CloseableHttpResponse createResponse(int statusCode, String jsonBody) { + CloseableHttpResponse responseMock = mock(CloseableHttpResponse.class); + StatusLine statusLineMock = mock(StatusLine.class); + when(responseMock.getStatusLine()).thenReturn(statusLineMock); + when(statusLineMock.getStatusCode()).thenReturn(statusCode); + + if (jsonBody != null) { + when(responseMock.getEntity()).thenReturn(new StringEntity(jsonBody, StandardCharsets.UTF_8)); + } + + return responseMock; + } + + private void mockHttpResponse(int statusCode, String jsonBody) throws IOException { + CloseableHttpResponse response = createResponse(statusCode, jsonBody); + when(httpClientMock.execute(any(HttpUriRequest.class))).thenReturn(response); + } @Test public void testNormalizeApexRecord() { @@ -80,4 +132,225 @@ public class PowerDnsClientTest { result = client.normalizeRecordName("www", "example.com."); assertEquals("www.example.com.", result); } + + @Test + public void testDiscoverAuthoritativeServerIdSuccess() throws Exception { + mockHttpResponse(200, "[{\"id\":\"localhost\", \"daemon_type\":\"authoritative\"}]"); + String result = client.discoverAuthoritativeServerId("http://pdns:8081", null, "apikey"); + assertEquals("localhost", result); + } + + @Test + public void testDiscoverAuthoritativeServerIdFallback() throws Exception { + mockHttpResponse(200, "[{\"id\":\"server1\", \"daemon_type\":\"recursor\"}, {\"id\":\"server2\", \"daemon_type\":\"authoritative\"}]"); + String result = client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey"); + assertEquals("server2", result); + } + + @Test(expected = DnsOperationException.class) + public void testDiscoverAuthoritativeServerIdEmpty() throws Exception { + mockHttpResponse(200, "[]"); + client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey"); + } + + @Test(expected = DnsOperationException.class) + public void testDiscoverAuthoritativeServerIdNoAuthoritative() throws Exception { + mockHttpResponse(200, "[{\"id\":\"server1\", \"daemon_type\":\"recursor\"}]"); + client.discoverAuthoritativeServerId("http://pdns", 8081, "apikey"); + } + + @Test + public void testValidateServerIdSuccess() throws Exception { + mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + String result = client.validateServerId("http://pdns", 8081, "apikey", "abc"); + assertEquals("abc", result); + } + + @Test(expected = DnsOperationException.class) + public void testValidateServerIdNotAuthoritative() throws Exception { + mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"recursor\"}"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test + public void testResolveServerIdWithExternalId() throws Exception { + mockHttpResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + String result = client.resolveServerId("http://pdns", 8081, "apikey", "abc"); + assertEquals("abc", result); + } + + @Test + public void testResolveServerIdWithoutExternalId() throws Exception { + mockHttpResponse(200, "[{\"id\":\"localhost\", \"daemon_type\":\"authoritative\"}]"); + String result = client.resolveServerId("http://pdns", 8081, "apikey", null); + assertEquals("localhost", result); + } + + @Test + public void testCreateZone() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("POST")) { + return createResponse(201, "{\"id\":\"example.com.\"}"); + } + return createResponse(500, null); + } + }); + + String result = client.createZone("http://pdns", 8081, "apikey", "abc", "example.com", "Native", false, Arrays.asList("ns1.com")); + assertEquals("example.com.", result); + } + + @Test + public void testUpdateZone() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("PUT")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + client.updateZone("http://pdns", 8081, "apikey", "abc", "example.com", "Native", true, Arrays.asList("ns1.com")); + // No exception means success + } + + @Test + public void testDeleteZone() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("DELETE")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + client.deleteZone("http://pdns", 8081, "apikey", "abc", "example.com"); + } + + @Test + public void testModifyRecord() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("PATCH")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + String result = client.modifyRecord("http://pdns", 8081, "apikey", "abc", "example.com", "www", "A", 300, Arrays.asList("1.2.3.4"), "REPLACE"); + assertEquals("www.example.com", result); + } + + @Test + public void testModifyRecordApex() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getMethod().equals("GET")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getMethod().equals("PATCH")) { + return createResponse(204, null); + } + return createResponse(500, null); + } + }); + + String result = client.modifyRecord("http://pdns", 8081, "apikey", "abc", "example.com", "@", "A", 300, Arrays.asList("1.2.3.4"), "REPLACE"); + assertEquals("example.com", result); + } + + @Test + public void testListRecords() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + // validateServerId uses /servers/abc + // listRecords uses /servers/abc/zones/example.com. + if (request.getURI().getPath().endsWith("abc")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getURI().getPath().endsWith("example.com.")) { + return createResponse(200, "{\"rrsets\":[{\"name\":\"www.example.com.\",\"type\":\"A\"}]}"); + } + return createResponse(500, null); + } + }); + + Iterable records = client.listRecords("http://pdns", 8081, "apikey", "abc", "example.com"); + assertNotNull(records); + assertTrue(records.iterator().hasNext()); + assertEquals("www.example.com.", records.iterator().next().path("name").asText()); + } + + @Test + public void testListRecordsEmpty() throws Exception { + when(httpClientMock.execute(any(HttpUriRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocation) { + HttpUriRequest request = invocation.getArgument(0); + if (request.getURI().getPath().endsWith("abc")) { + return createResponse(200, "{\"id\":\"abc\", \"daemon_type\":\"authoritative\"}"); + } + if (request.getURI().getPath().endsWith("example.com.")) { + return createResponse(200, "{}"); + } + return createResponse(500, null); + } + }); + + Iterable records = client.listRecords("http://pdns", 8081, "apikey", "abc", "example.com"); + assertNotNull(records); + assertTrue(!records.iterator().hasNext()); + } + + @Test(expected = DnsNotFoundException.class) + public void testExecuteThrowsNotFound() throws Exception { + mockHttpResponse(404, "Not Found"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test(expected = DnsAuthenticationException.class) + public void testExecuteThrowsAuthError() throws Exception { + mockHttpResponse(401, "Unauthorized"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test(expected = DnsConflictException.class) + public void testExecuteThrowsConflictError() throws Exception { + mockHttpResponse(409, "Conflict"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } + + @Test(expected = DnsOperationException.class) + public void testExecuteThrowsUnexpectedStatus() throws Exception { + mockHttpResponse(500, "Server Error"); + client.validateServerId("http://pdns", 8081, "apikey", "abc"); + } } diff --git a/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java new file mode 100644 index 00000000000..04fb9fa854e --- /dev/null +++ b/plugins/dns/powerdns/src/test/java/org/apache/cloudstack/dns/powerdns/PowerDnsProviderTest.java @@ -0,0 +1,391 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.dns.powerdns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; + + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.apache.cloudstack.dns.DnsRecord; +import org.apache.cloudstack.dns.DnsRecord.RecordType; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.exception.DnsProviderException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +@RunWith(MockitoJUnitRunner.class) +public class PowerDnsProviderTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private PowerDnsProvider provider; + private PowerDnsClient clientMock; + private DnsServer serverMock; + private DnsZone zoneMock; + + @Before + public void setUp() { + provider = new PowerDnsProvider(); + clientMock = mock(PowerDnsClient.class); + serverMock = mock(DnsServer.class); + zoneMock = mock(DnsZone.class); + ReflectionTestUtils.setField(provider, "client", clientMock); + + when(serverMock.getUrl()).thenReturn("http://pdns:8081"); + when(serverMock.getApiKey()).thenReturn("secret"); + when(serverMock.getPort()).thenReturn(8081); + when(serverMock.getExternalServerId()).thenReturn("localhost"); + when(serverMock.getNameServers()).thenReturn(Arrays.asList("ns1.example.com")); + + when(zoneMock.getName()).thenReturn("example.com"); + } + + @Test + public void testGetProviderType() { + assertEquals(DnsProviderType.PowerDNS, provider.getProviderType()); + } + + @Test + public void testConfigureCreatesClientWhenNull() { + PowerDnsProvider freshProvider = new PowerDnsProvider(); + boolean result = freshProvider.configure("test", new HashMap<>()); + assertTrue(result); + assertNotNull(ReflectionTestUtils.getField(freshProvider, "client")); + } + + @Test + public void testConfigureDoesNotReplaceExistingClient() { + PowerDnsClient existingClient = mock(PowerDnsClient.class); + ReflectionTestUtils.setField(provider, "client", existingClient); + + boolean result = provider.configure("test", new HashMap<>()); + + assertTrue(result); + assertEquals(existingClient, ReflectionTestUtils.getField(provider, "client")); + } + + @Test + public void testStopClosesClient() { + boolean result = provider.stop(); + assertTrue(result); + verify(clientMock, times(1)).close(); + } + + @Test + public void testStopWithNullClientSucceeds() { + ReflectionTestUtils.setField(provider, "client", null); + boolean result = provider.stop(); + assertTrue(result); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsNullUrl() { + when(serverMock.getUrl()).thenReturn(null); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsBlankUrl() { + when(serverMock.getUrl()).thenReturn(" "); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsNullApiKey() { + when(serverMock.getApiKey()).thenReturn(null); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerFieldsBlankApiKey() { + when(serverMock.getApiKey()).thenReturn(""); + provider.validateRequiredServerFields(serverMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerAndZoneFieldsBlankZoneName() { + when(zoneMock.getName()).thenReturn(" "); + provider.validateRequiredServerAndZoneFields(serverMock, zoneMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateServerAndZoneFieldsNullZoneName() { + when(zoneMock.getName()).thenReturn(null); + provider.validateRequiredServerAndZoneFields(serverMock, zoneMock); + } + + @Test + public void testValidateDelegatesToClient() throws DnsProviderException { + when(clientMock.validateServerId(anyString(), anyInt(), anyString(), anyString())).thenReturn("localhost"); + provider.validate(serverMock); + verify(clientMock).validateServerId("http://pdns:8081", 8081, "secret", "localhost"); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateThrowsWhenServerUrlBlank() throws DnsProviderException { + when(serverMock.getUrl()).thenReturn(""); + provider.validate(serverMock); + } + + @Test + public void testValidateAndResolveServer() throws Exception { + when(clientMock.resolveServerId(anyString(), anyInt(), anyString(), anyString())).thenReturn("localhost"); + String result = provider.validateAndResolveServer(serverMock); + assertEquals("localhost", result); + verify(clientMock).resolveServerId("http://pdns:8081", 8081, "secret", "localhost"); + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateAndResolveServerThrowsWhenUrlBlank() throws Exception { + when(serverMock.getUrl()).thenReturn(null); + provider.validateAndResolveServer(serverMock); + } + + @Test + public void testProvisionZoneDelegatesToClient() throws DnsProviderException { + when(clientMock.createZone(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(), eq(false), anyList())).thenReturn("example.com."); + String zoneId = provider.provisionZone(serverMock, zoneMock); + assertEquals("example.com.", zoneId); + verify(clientMock).createZone("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "Native", false, Arrays.asList("ns1.example.com")); + } + + @Test(expected = IllegalArgumentException.class) + public void testProvisionZoneThrowsWhenZoneNameBlank() throws DnsProviderException { + when(zoneMock.getName()).thenReturn(null); + provider.provisionZone(serverMock, zoneMock); + } + + @Test + public void testDeleteZoneDelegatesToClient() throws DnsProviderException { + provider.deleteZone(serverMock, zoneMock); + verify(clientMock).deleteZone("http://pdns:8081", 8081, "secret", "localhost", "example.com"); + } + + @Test(expected = IllegalArgumentException.class) + public void testDeleteZoneThrowsWhenZoneNameBlank() throws DnsProviderException { + when(zoneMock.getName()).thenReturn(""); + provider.deleteZone(serverMock, zoneMock); + } + + @Test + public void testUpdateZoneDelegatesToClient() throws DnsProviderException { + provider.updateZone(serverMock, zoneMock); + verify(clientMock).updateZone("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "Native", false, Arrays.asList("ns1.example.com")); + } + + @Test + public void testAddRecordDelegatesToClient() throws DnsProviderException { + DnsRecord record = new DnsRecord("www", RecordType.A, Arrays.asList("1.2.3.4"), 300); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("www.example.com"); + + String result = provider.addRecord(serverMock, zoneMock, record); + + assertEquals("www.example.com", result); + verify(clientMock).modifyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "www", "A", 300L, Arrays.asList("1.2.3.4"), "REPLACE"); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddRecordThrowsWhenServerUrlBlank() throws DnsProviderException { + when(serverMock.getUrl()).thenReturn(""); + DnsRecord record = new DnsRecord("www", RecordType.A, Arrays.asList("1.2.3.4"), 300); + provider.addRecord(serverMock, zoneMock, record); + } + + @Test + public void testUpdateRecordDelegatesToAddRecord() throws DnsProviderException { + DnsRecord record = new DnsRecord("mail", RecordType.MX, Arrays.asList("10 mail.example.com"), 300); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("mail.example.com"); + + String result = provider.updateRecord(serverMock, zoneMock, record); + + assertEquals("mail.example.com", result); + verify(clientMock).modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + eq("mail"), eq("MX"), eq(300L), eq(Arrays.asList("10 mail.example.com")), eq("REPLACE")); + } + + @Test + public void testDeleteRecordDelegatesToClientWithDeleteChangeType() throws DnsProviderException { + DnsRecord record = new DnsRecord("old", RecordType.CNAME, Arrays.asList("target.com"), 600); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("old.example.com"); + + String result = provider.deleteRecord(serverMock, zoneMock, record); + + assertEquals("old.example.com", result); + verify(clientMock).modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + eq("old"), eq("CNAME"), eq(600L), eq(Arrays.asList("target.com")), eq("DELETE")); + } + + @Test + public void testApplyRecordPassesChangeTypeToClient() throws DnsProviderException { + DnsRecord record = new DnsRecord("txt", RecordType.TXT, Arrays.asList("v=spf1 include:example.com ~all"), 3600); + when(clientMock.modifyRecord(anyString(), anyInt(), anyString(), anyString(), anyString(), + anyString(), anyString(), anyLong(), anyList(), anyString())).thenReturn("txt.example.com"); + + provider.applyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com", + record, PowerDnsProvider.ChangeType.REPLACE); + + verify(clientMock).modifyRecord("http://pdns:8081", 8081, "secret", "localhost", "example.com", + "txt", "TXT", 3600L, Arrays.asList("v=spf1 include:example.com ~all"), "REPLACE"); + } + + @Test + public void testListRecordsParsesRrsets() throws DnsProviderException { + ObjectNode aRecord = MAPPER.createObjectNode(); + aRecord.put("name", "www.example.com."); + aRecord.put("type", "A"); + aRecord.put("ttl", 300); + ArrayNode records = aRecord.putArray("records"); + records.addObject().put("content", "1.2.3.4"); + + ObjectNode mxRecord = MAPPER.createObjectNode(); + mxRecord.put("name", "example.com."); + mxRecord.put("type", "MX"); + mxRecord.put("ttl", 600); + ArrayNode mxRecords = mxRecord.putArray("records"); + mxRecords.addObject().put("content", "10 mail.example.com"); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Arrays.asList(aRecord, mxRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + + assertEquals(2, result.size()); + + DnsRecord first = result.get(0); + assertEquals("www.example.com.", first.getName()); + assertEquals(RecordType.A, first.getType()); + assertEquals(300, first.getTtl()); + assertEquals(Arrays.asList("1.2.3.4"), first.getContents()); + + DnsRecord second = result.get(1); + assertEquals("example.com.", second.getName()); + assertEquals(RecordType.MX, second.getType()); + assertEquals(600, second.getTtl()); + assertEquals(Arrays.asList("10 mail.example.com"), second.getContents()); + } + + @Test + public void testListRecordsSkipsSoaRecords() throws DnsProviderException { + ObjectNode soaRecord = MAPPER.createObjectNode(); + soaRecord.put("name", "example.com."); + soaRecord.put("type", "SOA"); + soaRecord.put("ttl", 3600); + soaRecord.putArray("records").addObject().put("content", "ns1.example.com. admin.example.com. ..."); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.singletonList(soaRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + assertTrue(result.isEmpty()); + } + + @Test + public void testListRecordsSkipsUnknownRecordTypes() throws DnsProviderException { + ObjectNode unknownRecord = MAPPER.createObjectNode(); + unknownRecord.put("name", "test.example.com."); + unknownRecord.put("type", "UNKNOWNTYPE"); + unknownRecord.put("ttl", 300); + unknownRecord.putArray("records").addObject().put("content", "some-data"); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.singletonList(unknownRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + assertTrue(result.isEmpty()); + } + + @Test + public void testListRecordsIgnoresEmptyContentEntries() throws DnsProviderException { + ObjectNode aRecord = MAPPER.createObjectNode(); + aRecord.put("name", "host.example.com."); + aRecord.put("type", "A"); + aRecord.put("ttl", 300); + ArrayNode records = aRecord.putArray("records"); + records.addObject().put("content", ""); + records.addObject().put("content", "5.6.7.8"); + + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.singletonList(aRecord)); + + List result = provider.listRecords(serverMock, zoneMock); + assertEquals(1, result.size()); + assertEquals(Collections.singletonList("5.6.7.8"), result.get(0).getContents()); + } + + @Test + public void testListRecordsReturnsEmptyListWhenClientReturnsEmpty() throws DnsProviderException { + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(Collections.emptyList()); + + List result = provider.listRecords(serverMock, zoneMock); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test(expected = DnsProviderException.class) + public void testListRecordsPropagatesClientException() throws DnsProviderException { + when(clientMock.listRecords(anyString(), anyInt(), anyString(), anyString(), anyString())) + .thenThrow(mock(DnsProviderException.class)); + + provider.listRecords(serverMock, zoneMock); + } + + @Test(expected = IllegalArgumentException.class) + public void testListRecordsThrowsWhenZoneNameBlank() throws DnsProviderException { + when(zoneMock.getName()).thenReturn(""); + provider.listRecords(serverMock, zoneMock); + } + + @Test + public void testChangeTypeValues() { + assertEquals("REPLACE", PowerDnsProvider.ChangeType.REPLACE.name()); + assertEquals("DELETE", PowerDnsProvider.ChangeType.DELETE.name()); + } +} diff --git a/server/src/main/java/com/cloud/event/ActionEventUtils.java b/server/src/main/java/com/cloud/event/ActionEventUtils.java index de1ffd72ad5..ae77446a856 100644 --- a/server/src/main/java/com/cloud/event/ActionEventUtils.java +++ b/server/src/main/java/com/cloud/event/ActionEventUtils.java @@ -34,7 +34,6 @@ import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.events.EventDistributor; -import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -86,8 +85,6 @@ public class ActionEventUtils { EntityManager entityMgr; @Inject ConfigurationDao configDao; - @Inject - MessageBus messageBus; public ActionEventUtils() { } @@ -100,7 +97,6 @@ public class ActionEventUtils { s_projectDao = projectDao; s_entityMgr = entityMgr; s_configDao = configDao; - messageBus = messageBus; } public static Long onActionEvent(Long userId, Long accountId, Long domainId, String type, String description, Long resourceId, String resourceType) { diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java b/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java deleted file mode 100644 index a235ce75022..00000000000 --- a/server/src/main/java/org/apache/cloudstack/dns/DnsVmLifecycleListener.java +++ /dev/null @@ -1,196 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -// - -package org.apache.cloudstack.dns; - -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; - -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.framework.events.Event; -import org.apache.cloudstack.framework.events.EventBus; -import org.apache.cloudstack.framework.events.EventBusException; -import org.apache.cloudstack.framework.events.EventSubscriber; -import org.apache.cloudstack.framework.events.EventTopic; -import org.springframework.stereotype.Component; - -import com.cloud.event.EventTypes; -import com.cloud.network.Network; -import com.cloud.network.dao.NetworkDao; -import com.cloud.utils.StringUtils; -import com.cloud.utils.component.ManagerBase; -import com.cloud.vm.Nic; -import com.cloud.vm.NicVO; -import com.cloud.vm.VMInstanceVO; -import com.cloud.vm.dao.NicDao; -import com.cloud.vm.dao.NicDetailsDao; -import com.cloud.vm.dao.VMInstanceDao; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -@Component -public class DnsVmLifecycleListener extends ManagerBase implements EventSubscriber { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - @Inject - private EventBus eventBus = null; - - @Inject - VMInstanceDao vmInstanceDao; - @Inject - NetworkDao networkDao; - @Inject - NicDao nicDao; - @Inject - DnsProviderManager providerManager; - @Inject - NicDetailsDao nicDetailsDao; - - @Override - public boolean configure(final String name, final Map params) { - if (eventBus == null) { - logger.info("EventBus is not available; DNS Instance lifecycle listener will not subscribe to events"); - return true; - } - try { - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_CREATE, null, null, null), this); - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_START, null, null, null), this); - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_STOP, null, null, null), this); - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_VM_DESTROY, null, null, null), this); - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_CREATE, null, null, null), this); - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_NIC_DELETE, null, null, null), this); - eventBus.subscribe(new EventTopic(null, EventTypes.EVENT_DNS_RECORD_DELETE, null, null, null), this); - } catch (EventBusException ex) { - logger.error("Failed to subscribe DnsVmLifecycleListener to EventBus", ex); - } - return true; - } - - @Override - public void onEvent(Event event) { - JsonNode descJson = parseEventDescription(event); - if (!isEventCompleted(descJson)) { - return; - } - - String eventType = event.getEventType(); - String resourceUuid = event.getResourceUUID(); - try { - switch (eventType) { - case EventTypes.EVENT_VM_CREATE: - case EventTypes.EVENT_VM_START: - handleVmEvent(resourceUuid, true); - break; - case EventTypes.EVENT_VM_STOP: - case EventTypes.EVENT_VM_DESTROY: - handleVmEvent(resourceUuid, false); - break; - case EventTypes.EVENT_NIC_CREATE: - handleNicEvent(descJson, true); - break; - case EventTypes.EVENT_NIC_DELETE: - handleNicEvent(descJson, false); - break; - default: - break; - } - } catch (Exception ex) { - logger.error("Failed to process DNS lifecycle event: type={}, resourceUuid={}", - eventType, event.getResourceUUID(), ex); - } - } - - private void handleNicEvent(JsonNode eventDesc, boolean isAddDnsRecord) { - JsonNode nicUuid = eventDesc.get("Nic"); - JsonNode vmUuid = eventDesc.get("VirtualMachine"); - if (nicUuid == null || nicUuid.isNull() || vmUuid == null || vmUuid.isNull()) { - logger.warn("Event has missing data to work on: {}", eventDesc); - return; - } - VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuid(vmUuid.asText()); - if (vmInstanceVO == null) { - logger.error("Unable to find Instance with ID: {}", vmUuid); - return; - } - Nic nic = nicDao.findByUuidIncludingRemoved(nicUuid.asText()); - if (nic == null) { - logger.error("NIC is not found for the ID: {}", nicUuid); - return; - } - Network network = networkDao.findById(nic.getNetworkId()); - if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { - logger.warn("Network is not eligible for DNS record registration"); - return; - } - processEventForDnsRecord(vmInstanceVO, network, nic, isAddDnsRecord); - } - - private void handleVmEvent(String vmUuid, boolean isAddDnsRecord) { - VMInstanceVO vmInstanceVO = vmInstanceDao.findByUuidIncludingRemoved(vmUuid); - if (vmInstanceVO == null) { - logger.error("Unable to find Instance with ID: {}", vmUuid); - return; - } - List vmNics = nicDao.listByVmIdIncludingRemoved(vmInstanceVO.getId()); - for (NicVO nic : vmNics) { - Network network = networkDao.findById(nic.getNetworkId()); - if (network == null || !Network.GuestType.Shared.equals(network.getGuestType())) { - continue; - } - processEventForDnsRecord(vmInstanceVO, network, nic, isAddDnsRecord); - } - } - - void processEventForDnsRecord(VMInstanceVO vmInstanceVO, Network network, Nic nic, boolean isAddDnsRecord) { - if (isAddDnsRecord) { - providerManager.addDnsRecordForVM(vmInstanceVO, network, nic); - } else { - providerManager.deleteDnsRecordForVM(vmInstanceVO, network, nic); - } - } - - private JsonNode parseEventDescription(Event event) { - String rawDescription = event.getDescription(); - if (StringUtils.isBlank(rawDescription)) { - return null; - } - try { - return OBJECT_MAPPER.readTree(rawDescription); - } catch (Exception ex) { - logger.warn("parseEventDescription: failed to parse description for event [{}]: {}", - event.getEventType(), ex.getMessage()); - return null; - } - } - - private boolean isEventCompleted(JsonNode descJson) { - if (descJson == null) { - return false; - } - JsonNode statusNode = descJson.get(ApiConstants.STATUS); - if (statusNode == null || statusNode.isNull()) { - return false; - } - - logger.debug("Processing Event: {}", descJson); - return ApiConstants.COMPLETED.equalsIgnoreCase(statusNode.asText()); - } -} diff --git a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java new file mode 100644 index 00000000000..c282c982e0e --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java @@ -0,0 +1,892 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.dns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.cloudstack.api.command.user.dns.CreateDnsZoneCmd; +import org.apache.cloudstack.api.command.user.dns.DeleteDnsServerCmd; + +import org.apache.cloudstack.api.command.user.dns.DisassociateDnsZoneFromNetworkCmd; +import org.apache.cloudstack.api.command.user.dns.ListDnsRecordsCmd; +import org.apache.cloudstack.api.command.user.dns.UpdateDnsZoneCmd; +import org.apache.cloudstack.api.response.DnsRecordResponse; +import org.apache.cloudstack.api.response.DnsServerResponse; +import org.apache.cloudstack.api.response.DnsZoneNetworkMapResponse; +import org.apache.cloudstack.api.response.DnsZoneResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.dns.dao.DnsServerDao; +import org.apache.cloudstack.dns.dao.DnsServerJoinDao; +import org.apache.cloudstack.dns.dao.DnsZoneDao; +import org.apache.cloudstack.dns.dao.DnsZoneJoinDao; +import org.apache.cloudstack.dns.dao.DnsZoneNetworkMapDao; +import org.apache.cloudstack.dns.exception.DnsConflictException; +import org.apache.cloudstack.dns.exception.DnsNotFoundException; +import org.apache.cloudstack.dns.exception.DnsProviderException; +import org.apache.cloudstack.dns.exception.DnsTransportException; +import org.apache.cloudstack.dns.vo.DnsServerJoinVO; +import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.apache.cloudstack.dns.vo.DnsZoneJoinVO; +import org.apache.cloudstack.dns.vo.DnsZoneNetworkMapVO; +import org.apache.cloudstack.dns.vo.DnsZoneVO; +import org.apache.cloudstack.framework.messagebus.MessageBus; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import com.cloud.domain.dao.DomainDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.network.Network; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.utils.db.Transaction; +import com.cloud.utils.db.TransactionCallback; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.NicDetailVO; +import com.cloud.vm.NicVO; +import com.cloud.vm.VMInstanceVO; +import com.cloud.vm.dao.NicDao; +import com.cloud.vm.dao.NicDetailsDao; +import com.cloud.vm.dao.VMInstanceDao; + +@RunWith(MockitoJUnitRunner.class) +public class DnsProviderManagerImplTest { + + private static final long ACCOUNT_ID = 1L; + private static final long DOMAIN_ID = 10L; + private static final long SERVER_ID = 100L; + private static final long ZONE_ID = 200L; + private static final long NETWORK_ID = 300L; + + @InjectMocks + DnsProviderManagerImpl manager; + + @Mock AccountManager accountMgr; + @Mock DnsServerDao dnsServerDao; + @Mock DnsZoneDao dnsZoneDao; + @Mock DnsZoneJoinDao dnsZoneJoinDao; + @Mock DnsServerJoinDao dnsServerJoinDao; + @Mock DnsZoneNetworkMapDao dnsZoneNetworkMapDao; + @Mock NetworkDao networkDao; + @Mock DomainDao domainDao; + @Mock NicDao nicDao; + @Mock NicDetailsDao nicDetailsDao; + @Mock MessageBus messageBus; + @Mock VMInstanceDao vmInstanceDao; + @Mock DnsProvider dnsProviderMock; + @Mock Account callerMock; + + private MockedStatic callContextMocked; + private CallContext callContextMock; + + // Support VOs + private DnsServerVO serverVO; + private DnsZoneVO zoneVO; + + @Before + public void setUp() throws Exception { + callContextMocked = Mockito.mockStatic(CallContext.class); + callContextMock = mock(CallContext.class); + callContextMocked.when(CallContext::current).thenReturn(callContextMock); + when(callContextMock.getCallingAccount()).thenReturn(callerMock); + when(callerMock.getId()).thenReturn(ACCOUNT_ID); + when(callerMock.getDomainId()).thenReturn(DOMAIN_ID); + + serverVO = Mockito.spy(new DnsServerVO("test-server", "http://pdns:8081", 8081, "localhost", + DnsProviderType.PowerDNS, null, "apikey", false, null, + Collections.singletonList("ns1.example.com"), ACCOUNT_ID, DOMAIN_ID)); + Mockito.lenient().doReturn(SERVER_ID).when(serverVO).getId(); + + zoneVO = Mockito.spy(new DnsZoneVO("example.com", DnsZone.ZoneType.Public, SERVER_ID, ACCOUNT_ID, DOMAIN_ID, "Test zone")); + Mockito.lenient().doReturn(ZONE_ID).when(zoneVO).getId(); + + when(dnsProviderMock.getProviderType()).thenReturn(DnsProviderType.PowerDNS); + manager.setDnsProviders(Collections.singletonList(dnsProviderMock)); + + doNothing().when(accountMgr).checkAccess(any(Account.class), nullable(org.apache.cloudstack.acl.SecurityChecker.AccessType.class), eq(true), any()); + } + + @After + public void tearDown() { + callContextMocked.close(); + } + + @Test(expected = CloudRuntimeException.class) + public void testGetProviderByTypeNull() { + // Setting providers to empty to force lookup failure + manager.setDnsProviders(Collections.emptyList()); + // Trigger via provisionDnsZone which calls getProviderByType + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + manager.provisionDnsZone(ZONE_ID); + } + + @Test + public void testListProviderNamesReturnsList() { + List names = manager.listProviderNames(); + assertEquals(1, names.size()); + assertEquals("PowerDNS", names.get(0)); + } + + @Test + public void testListProviderNamesWithNullProviders() { + manager.setDnsProviders(null); + List names = manager.listProviderNames(); + assertTrue(names.isEmpty()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAllocateDnsZoneBlankName() { + CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class); + when(cmd.getName()).thenReturn(" "); + manager.allocateDnsZone(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAllocateDnsZoneServerNotFound() { + CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class); + when(cmd.getName()).thenReturn("example.com"); + when(cmd.getDnsServerId()).thenReturn(SERVER_ID); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); + manager.allocateDnsZone(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAllocateDnsZoneAlreadyExists() { + CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class); + when(cmd.getName()).thenReturn("example.com"); + when(cmd.getDnsServerId()).thenReturn(SERVER_ID); + when(cmd.getType()).thenReturn(DnsZone.ZoneType.Public); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + Mockito.doReturn(SERVER_ID).when(serverVO).getId(); + Mockito.doReturn(ACCOUNT_ID).when(serverVO).getAccountId(); + when(dnsZoneDao.findByNameServerAndType(anyString(), anyLong(), any())).thenReturn(zoneVO); + manager.allocateDnsZone(cmd); + } + + @Test + public void testAllocateDnsZoneOwnerSuccess() { + CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class); + when(cmd.getName()).thenReturn("example.com"); + when(cmd.getDnsServerId()).thenReturn(SERVER_ID); + when(cmd.getType()).thenReturn(DnsZone.ZoneType.Public); + when(cmd.getDescription()).thenReturn("desc"); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + Mockito.doReturn(SERVER_ID).when(serverVO).getId(); + Mockito.doReturn(ACCOUNT_ID).when(serverVO).getAccountId(); + when(dnsZoneDao.findByNameServerAndType(anyString(), anyLong(), any())).thenReturn(null); + when(dnsZoneDao.persist(any(DnsZoneVO.class))).thenReturn(zoneVO); + DnsZone result = manager.allocateDnsZone(cmd); + assertNotNull(result); + } + + @Test(expected = PermissionDeniedException.class) + public void testAllocateDnsZoneNonOwnerPrivateServer() { + CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class); + when(cmd.getName()).thenReturn("tenant.com"); + when(cmd.getDnsServerId()).thenReturn(SERVER_ID); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + Mockito.doReturn(ACCOUNT_ID + 99).when(serverVO).getAccountId(); // different owner + + manager.allocateDnsZone(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testProvisionDnsZoneNotFound() { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(null); + manager.provisionDnsZone(ZONE_ID); + } + + @Test + public void testProvisionDnsZoneSuccess() throws Exception { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + when(dnsProviderMock.provisionZone(any(), any())).thenReturn("example.com."); + when(dnsZoneDao.update(anyLong(), any())).thenReturn(true); + DnsZone result = manager.provisionDnsZone(ZONE_ID); + assertNotNull(result); + verify(dnsProviderMock).provisionZone(serverVO, zoneVO); + verify(dnsZoneDao).update(anyLong(), any()); + } + + @Test(expected = CloudRuntimeException.class) + public void testProvisionDnsZoneConflictException() throws Exception { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + when(dnsProviderMock.provisionZone(any(), any())).thenThrow(new DnsConflictException("conflict")); + manager.provisionDnsZone(ZONE_ID); + verify(dnsZoneDao).remove(ZONE_ID); + } + + @Test(expected = CloudRuntimeException.class) + public void testProvisionDnsZoneTransportException() throws Exception { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + when(dnsProviderMock.provisionZone(any(), any())).thenThrow(new DnsTransportException("unreachable", new IOException("i/o"))); + manager.provisionDnsZone(ZONE_ID); + verify(dnsZoneDao).remove(ZONE_ID); + } + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteDnsZoneNotFound() { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(null); + manager.deleteDnsZone(ZONE_ID); + } + + @Test(expected = CloudRuntimeException.class) + public void testDeleteDnsZoneServerMissing() { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); + manager.deleteDnsZone(ZONE_ID); + } + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateDnsZoneNotFound() { + UpdateDnsZoneCmd cmd = mock(UpdateDnsZoneCmd.class); + when(cmd.getId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(null); + manager.updateDnsZone(cmd); + } + + @Test + public void testUpdateDnsZoneNoChange() { + UpdateDnsZoneCmd cmd = mock(UpdateDnsZoneCmd.class); + when(cmd.getId()).thenReturn(ZONE_ID); + when(cmd.getDescription()).thenReturn(null); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + DnsZone result = manager.updateDnsZone(cmd); + assertNotNull(result); + verify(dnsZoneDao, never()).update(anyLong(), any()); + } + + @Test + public void testUpdateDnsZoneWithDescription() throws Exception { + UpdateDnsZoneCmd cmd = mock(UpdateDnsZoneCmd.class); + when(cmd.getId()).thenReturn(ZONE_ID); + when(cmd.getDescription()).thenReturn("Updated description"); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + doNothing().when(dnsProviderMock).updateZone(any(), any()); + when(dnsZoneDao.update(anyLong(), any())).thenReturn(true); + DnsZone result = manager.updateDnsZone(cmd); + assertNotNull(result); + verify(dnsProviderMock).updateZone(serverVO, zoneVO); + } + + @Test(expected = CloudRuntimeException.class) + public void testUpdateDnsZoneServerMissing() { + UpdateDnsZoneCmd cmd = mock(UpdateDnsZoneCmd.class); + when(cmd.getId()).thenReturn(ZONE_ID); + when(cmd.getDescription()).thenReturn("New description"); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); + manager.updateDnsZone(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteDnsServerNotFound() { + DeleteDnsServerCmd cmd = mock(DeleteDnsServerCmd.class); + when(cmd.getId()).thenReturn(SERVER_ID); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); + manager.deleteDnsServer(cmd); + } + + @Test + public void testDeleteDnsServerWithCleanup() throws Exception { + DeleteDnsServerCmd cmd = mock(DeleteDnsServerCmd.class); + when(cmd.getId()).thenReturn(SERVER_ID); + when(cmd.getCleanup()).thenReturn(true); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + doNothing().when(accountMgr).checkAccess(any(Account.class), nullable(org.apache.cloudstack.acl.SecurityChecker.AccessType.class), eq(true), any()); + + List zones = Collections.singletonList(zoneVO); + when(dnsZoneDao.findDnsZonesByServerId(SERVER_ID)).thenReturn(zones); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsZoneNetworkMapDao.findByZoneId(ZONE_ID)).thenReturn(null); + when(dnsServerDao.remove(SERVER_ID)).thenReturn(true); + when(dnsZoneDao.remove(ZONE_ID)).thenReturn(true); + + try (MockedStatic transactionMock = Mockito.mockStatic(Transaction.class)) { + transactionMock.when(() -> Transaction.execute(any(TransactionCallback.class))).thenAnswer(invocation -> { + TransactionCallback callback = invocation.getArgument(0); + return callback.doInTransaction(null); + }); + + boolean res = manager.deleteDnsServer(cmd); + assertTrue(res); + verify(dnsServerDao).remove(SERVER_ID); + verify(dnsProviderMock).deleteZone(any(), any()); + } + } + + @Test + public void testDeleteDnsZoneSuccess() throws Exception { + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(anyLong())).thenReturn(serverVO); + doNothing().when(accountMgr).checkAccess(any(Account.class), nullable(org.apache.cloudstack.acl.SecurityChecker.AccessType.class), eq(true), any()); + when(dnsZoneNetworkMapDao.findByZoneId(ZONE_ID)).thenReturn(null); + when(dnsZoneDao.remove(ZONE_ID)).thenReturn(true); + + try (MockedStatic transactionMock = Mockito.mockStatic(Transaction.class)) { + transactionMock.when(() -> Transaction.execute(any(TransactionCallback.class))).thenAnswer(invocation -> { + TransactionCallback callback = invocation.getArgument(0); + return callback.doInTransaction(null); + }); + + boolean res = manager.deleteDnsZone(ZONE_ID); + assertTrue(res); + verify(dnsZoneDao).remove(ZONE_ID); + verify(dnsProviderMock).deleteZone(any(), any()); + } + } + + @Test(expected = InvalidParameterValueException.class) + public void testListDnsRecordsZoneNotFound() { + ListDnsRecordsCmd cmd = mock(ListDnsRecordsCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(null); + manager.listDnsRecords(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testListDnsRecordsServerMissing() { + ListDnsRecordsCmd cmd = mock(ListDnsRecordsCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); + manager.listDnsRecords(cmd); + } + + @Test + public void testListDnsRecordsSuccess() throws Exception { + ListDnsRecordsCmd cmd = mock(ListDnsRecordsCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + DnsRecord record = new DnsRecord("www.example.com", DnsRecord.RecordType.A, Collections.singletonList("1.2.3.4"), 300); + when(dnsProviderMock.listRecords(any(), any())).thenReturn(Collections.singletonList(record)); + ListResponse result = manager.listDnsRecords(cmd); + assertNotNull(result); + assertEquals(1, result.getCount().intValue()); + } + + @Test(expected = CloudRuntimeException.class) + public void testListDnsRecordsZoneNotFoundInProvider() throws Exception { + ListDnsRecordsCmd cmd = mock(ListDnsRecordsCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + when(dnsProviderMock.listRecords(any(), any())).thenThrow(new DnsNotFoundException("not found")); + manager.listDnsRecords(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testDisassociateZoneNoMappingFound() { + DisassociateDnsZoneFromNetworkCmd cmd = mock(DisassociateDnsZoneFromNetworkCmd.class); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(null); + manager.disassociateZoneFromNetwork(cmd); + } + + @Test + public void testDisassociateZoneOrphanedMapping() { + DisassociateDnsZoneFromNetworkCmd cmd = mock(DisassociateDnsZoneFromNetworkCmd.class); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); + when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); + when(mapping.getId()).thenReturn(500L); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(null); // zone missing (orphan) + when(dnsZoneNetworkMapDao.remove(500L)).thenReturn(true); + boolean result = manager.disassociateZoneFromNetwork(cmd); + assertTrue(result); + } + + @Test + public void testDisassociateZoneSuccess() { + DisassociateDnsZoneFromNetworkCmd cmd = mock(DisassociateDnsZoneFromNetworkCmd.class); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); + when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); + when(mapping.getId()).thenReturn(500L); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + + when(dnsZoneNetworkMapDao.remove(500L)).thenReturn(true); + boolean result = manager.disassociateZoneFromNetwork(cmd); + assertTrue(result); + verify(dnsZoneNetworkMapDao).remove(500L); + } + + @Test + public void testCreateDnsRecordResponse() { + DnsRecord record = new DnsRecord("www.example.com", DnsRecord.RecordType.A, Arrays.asList("1.2.3.4"), 300); + DnsRecordResponse response = manager.createDnsRecordResponse(record); + assertNotNull(response); + } + + @Test + public void testCreateDnsServerResponseFromJoinVO() { + DnsServerJoinVO join = mock(DnsServerJoinVO.class); + when(join.getUuid()).thenReturn("uuid-1"); + when(join.getName()).thenReturn("pdns"); + when(join.getUrl()).thenReturn("http://pdns:8081"); + when(join.getPort()).thenReturn(8081); + when(join.getProviderType()).thenReturn(DnsProviderType.PowerDNS.toString()); + when(join.isPublicServer()).thenReturn(false); + when(join.getNameServers()).thenReturn(Collections.emptyList()); + when(join.getPublicDomainSuffix()).thenReturn(null); + when(join.getAccountName()).thenReturn("admin"); + when(join.getDomainUuid()).thenReturn("domain-uuid"); + when(join.getDomainName()).thenReturn("ROOT"); + when(join.getState()).thenReturn(DnsServer.State.Enabled); + DnsServerResponse response = manager.createDnsServerResponse(join); + assertNotNull(response); + } + + @Test + public void testCreateDnsZoneResponseFromJoinVO() { + DnsZoneJoinVO join = mock(DnsZoneJoinVO.class); + when(join.getUuid()).thenReturn("zone-uuid"); + when(join.getName()).thenReturn("example.com"); + when(join.getDnsServerUuid()).thenReturn("server-uuid"); + when(join.getAccountName()).thenReturn("admin"); + when(join.getDomainUuid()).thenReturn("domain-uuid"); + when(join.getDomainName()).thenReturn("ROOT"); + when(join.getDnsServerName()).thenReturn("pdns"); + when(join.getDnsServerAccountName()).thenReturn("admin"); + when(join.getState()).thenReturn(DnsZone.State.Active); + when(join.getDescription()).thenReturn("Test zone"); + DnsZoneResponse response = manager.createDnsZoneResponse(join); + assertNotNull(response); + } + + @Test + public void testCheckDnsServerPermissionOwner() { + // owner has same accountId as server + when(callerMock.getId()).thenReturn(ACCOUNT_ID); + Mockito.doReturn(ACCOUNT_ID).when(serverVO).getAccountId(); + // Should not throw + manager.checkDnsServerPermission(callerMock, serverVO); + } + + @Test(expected = PermissionDeniedException.class) + public void testCheckDnsServerPermissionNonOwnerPrivate() { + when(callerMock.getId()).thenReturn(ACCOUNT_ID + 1); + Mockito.doReturn(ACCOUNT_ID).when(serverVO).getAccountId(); + Mockito.doReturn(false).when(serverVO).getPublicServer(); + manager.checkDnsServerPermission(callerMock, serverVO); + } + + @Test(expected = PermissionDeniedException.class) + public void testCheckDnsServerPermissionNonOwnerPublicOutsideDomain() { + AccountVO serverOwner = mock(AccountVO.class); + when(callerMock.getId()).thenReturn(ACCOUNT_ID + 1); + Mockito.doReturn(ACCOUNT_ID).when(serverVO).getAccountId(); + Mockito.doReturn(true).when(serverVO).getPublicServer(); + when(serverOwner.getDomainId()).thenReturn(20L); + when(callerMock.getDomainId()).thenReturn(DOMAIN_ID); + ReflectionTestUtils.setField(manager, "accountDao", + Mockito.mock(com.cloud.user.dao.AccountDao.class)); + com.cloud.user.dao.AccountDao accountDaoMock = (com.cloud.user.dao.AccountDao) ReflectionTestUtils.getField(manager, "accountDao"); + when(accountDaoMock.findByIdIncludingRemoved(ACCOUNT_ID)).thenReturn(serverOwner); + when(domainDao.isChildDomain(20L, DOMAIN_ID)).thenReturn(false); + manager.checkDnsServerPermission(callerMock, serverVO); + } + + @Test + public void testCheckDnsZonePermissionOwner() { + when(callerMock.getId()).thenReturn(ACCOUNT_ID); + Mockito.doReturn(ACCOUNT_ID).when(zoneVO).getAccountId(); + // Should not throw + manager.checkDnsZonePermission(callerMock, zoneVO); + } + + @Test(expected = PermissionDeniedException.class) + public void testCheckDnsZonePermissionNonOwner() { + when(callerMock.getId()).thenReturn(ACCOUNT_ID + 1); + Mockito.doReturn(ACCOUNT_ID).when(zoneVO).getAccountId(); + manager.checkDnsZonePermission(callerMock, zoneVO); + } + + @Test + public void testAddDnsRecordForVMNoNetworkMapping() throws DnsProviderException { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(dnsZoneNetworkMapDao.findByNetworkId(anyLong())).thenReturn(null); + when(network.getId()).thenReturn(NETWORK_ID); + manager.addDnsRecordForVM(vm, network, nic); + verify(dnsProviderMock, never()).addRecord(any(), any(), any()); + } + + @Test + public void testAddDnsRecordForVMInactiveZone() { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); + when(network.getId()).thenReturn(NETWORK_ID); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); + when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); + DnsZoneVO inactiveZone = Mockito.spy(new DnsZoneVO("ex.com", DnsZone.ZoneType.Public, SERVER_ID, ACCOUNT_ID, DOMAIN_ID, "")); + // state defaults to Inactive + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(inactiveZone); + manager.addDnsRecordForVM(vm, network, nic); + verify(dnsServerDao, never()).findById(anyLong()); + } + + @Test + public void testAddDnsRecordForVMServerMissing() { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); + when(network.getId()).thenReturn(NETWORK_ID); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); + when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); + DnsZoneVO activeZone = Mockito.spy(new DnsZoneVO("ex.com", DnsZone.ZoneType.Public, SERVER_ID, ACCOUNT_ID, DOMAIN_ID, "")); + activeZone.setState(DnsZone.State.Active); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(activeZone); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(null); + when(vm.getInstanceName()).thenReturn("vm-1"); + manager.addDnsRecordForVM(vm, network, nic); + verify(nicDetailsDao, never()).addDetail(anyLong(), anyString(), anyString(), eq(true)); + } + + @Test + public void testDeleteDnsRecordForVMNoNicDetail() { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + when(nic.getId()).thenReturn(50L); + when(vm.getInstanceName()).thenReturn("vm-1"); + when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(null); + manager.deleteDnsRecordForVM(vm, network, nic); + verify(dnsZoneNetworkMapDao, never()).findByNetworkId(anyLong()); + } + + @Test + public void testDeleteDnsRecordForVMNicDetailBlankValue() { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + NicDetailVO detail = mock(NicDetailVO.class); + when(nic.getId()).thenReturn(50L); + when(vm.getInstanceName()).thenReturn("vm-1"); + when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(detail); + when(detail.getValue()).thenReturn(" "); + manager.deleteDnsRecordForVM(vm, network, nic); + verify(dnsZoneNetworkMapDao, never()).findByNetworkId(anyLong()); + } + + @Test + public void testProcessEventForDnsRecordAdd() throws Exception { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + + when(dnsZoneNetworkMapDao.findByNetworkId(anyLong())).thenReturn(null); + when(network.getId()).thenReturn(NETWORK_ID); + manager.processEventForDnsRecord(vm, network, nic, true); + // addDnsRecordForVM was called → returns early because no mapping + verify(dnsZoneNetworkMapDao, times(1)).findByNetworkId(NETWORK_ID); + } + + @Test + public void testProcessEventForDnsRecordDelete() { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + VMInstanceVO vm = mock(VMInstanceVO.class); + + when(nic.getId()).thenReturn(50L); + when(vm.getInstanceName()).thenReturn("vm-1"); + when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(null); + manager.processEventForDnsRecord(vm, network, nic, false); + verify(nicDetailsDao, times(1)).findDetail(50L, "nicdnsrecord"); + } + + @Test + public void testGetCommandsReturnsNonEmptyList() { + List> commands = manager.getCommands(); + assertNotNull(commands); + assertFalse(commands.isEmpty()); + assertTrue(commands.size() > 5); + } + + @Test + public void testStartWithNoProviders() { + manager.setDnsProviders(Collections.emptyList()); + assertTrue(manager.start()); + } + + @Test + public void testStartWithProviders() { + assertTrue(manager.start()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAssociateZoneToNetworkZoneNotFound() { + org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd cmd = + mock(org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(null); + manager.associateZoneToNetwork(cmd); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAssociateZoneToNetworkNetworkNotFound() { + org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd cmd = + mock(org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + when(networkDao.findById(NETWORK_ID)).thenReturn(null); + manager.associateZoneToNetwork(cmd); + } + + @Test(expected = CloudRuntimeException.class) + public void testAssociateZoneToNetworkNonSharedNetwork() { + org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd cmd = + mock(org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + NetworkVO network = mock(NetworkVO.class); + when(network.getGuestType()).thenReturn(NetworkVO.GuestType.Isolated); + when(networkDao.findById(NETWORK_ID)).thenReturn(network); + manager.associateZoneToNetwork(cmd); + } + + @Test + public void testAssociateZoneToNetworkSuccess() { + org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd cmd = + mock(org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + Mockito.doReturn("zone-uuid").when(zoneVO).getUuid(); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + NetworkVO network = mock(NetworkVO.class); + when(network.getGuestType()).thenReturn(NetworkVO.GuestType.Shared); + + when(networkDao.findById(NETWORK_ID)).thenReturn(network); + DnsZoneNetworkMapVO savedMapping = mock(DnsZoneNetworkMapVO.class); + when(dnsZoneNetworkMapDao.persist(any(DnsZoneNetworkMapVO.class))).thenReturn(savedMapping); + DnsZoneNetworkMapResponse response = manager.associateZoneToNetwork(cmd); + assertNotNull(response); + verify(dnsZoneNetworkMapDao).persist(any(DnsZoneNetworkMapVO.class)); + } + + @Test(expected = InvalidParameterValueException.class) + public void testAssociateZoneToNetworkAlreadyAssociated() { + org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd cmd = + mock(org.apache.cloudstack.api.command.user.dns.AssociateDnsZoneToNetworkCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(cmd.getNetworkId()).thenReturn(NETWORK_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + NetworkVO network = mock(NetworkVO.class); + when(network.getGuestType()).thenReturn(NetworkVO.GuestType.Shared); + when(network.getId()).thenReturn(NETWORK_ID); + when(networkDao.findById(NETWORK_ID)).thenReturn(network); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mock(DnsZoneNetworkMapVO.class)); + manager.associateZoneToNetwork(cmd); + } + + @Test + public void testCreateDnsRecordSuccess() throws Exception { + org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.CreateDnsRecordCmd.class); + when(cmd.getName()).thenReturn("www"); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(cmd.getType()).thenReturn(DnsRecord.RecordType.A); + when(cmd.getContents()).thenReturn(Collections.singletonList("1.2.3.4")); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(anyLong())).thenReturn(serverVO); + when(dnsProviderMock.addRecord(any(), any(), any())).thenReturn("www.example.com"); + + DnsRecordResponse res = manager.createDnsRecord(cmd); + assertNotNull(res); + verify(dnsProviderMock).addRecord(any(), any(), any()); + } + + @Test + public void testDeleteDnsRecordSuccess() throws Exception { + org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.DeleteDnsRecordCmd.class); + when(cmd.getDnsZoneId()).thenReturn(ZONE_ID); + when(cmd.getName()).thenReturn("www"); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(anyLong())).thenReturn(serverVO); + when(dnsProviderMock.deleteRecord(any(), any(), any())).thenReturn("www.example.com"); + + boolean res = manager.deleteDnsRecord(cmd); + assertTrue(res); + verify(dnsProviderMock).deleteRecord(any(), any(), any()); + } + + @Test + public void testDeleteDnsRecordForVMSuccess() throws Exception { + Network network = mock(Network.class); + NicVO nic = mock(NicVO.class); + when(nic.getIPv4Address()).thenReturn("1.2.3.4"); + VMInstanceVO vm = mock(VMInstanceVO.class); + NicDetailVO detail = mock(NicDetailVO.class); + when(nic.getId()).thenReturn(50L); + when(vm.getInstanceName()).thenReturn("vm-1"); + when(nicDetailsDao.findDetail(50L, "nicdnsrecord")).thenReturn(detail); + when(detail.getValue()).thenReturn("vm-1.ex.com"); + + DnsZoneNetworkMapVO mapping = mock(DnsZoneNetworkMapVO.class); + when(network.getId()).thenReturn(NETWORK_ID); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(mapping); + when(mapping.getDnsZoneId()).thenReturn(ZONE_ID); + when(dnsZoneDao.findById(ZONE_ID)).thenReturn(zoneVO); + when(dnsServerDao.findById(anyLong())).thenReturn(serverVO); + when(dnsProviderMock.deleteRecord(any(), any(), any())).thenReturn("vm-1.ex.com"); + + manager.deleteDnsRecordForVM(vm, network, nic); + verify(dnsProviderMock).deleteRecord(any(), any(), any()); + verify(nicDetailsDao).removeDetail(50L, "nicdnsrecord"); + } + + @Test + public void testConfigure() throws Exception { + assertTrue(manager.configure("dnsProviderManagerImpl", Collections.emptyMap())); + verify(messageBus, times(3)).subscribe(anyString(), any()); + } + + @Test + public void testHandleVmEventAndNicEvent() throws Exception { + VMInstanceVO vm = mock(VMInstanceVO.class); + NicVO nic = mock(NicVO.class); + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(NETWORK_ID); + + when(vmInstanceDao.findById(10L)).thenReturn(vm); + when(nicDao.findByIdIncludingRemoved(50L)).thenReturn(nic); + when(nic.getNetworkId()).thenReturn(NETWORK_ID); + when(networkDao.findById(NETWORK_ID)).thenReturn(network); + when(network.getGuestType()).thenReturn(Network.GuestType.Shared); + when(dnsZoneNetworkMapDao.findByNetworkId(NETWORK_ID)).thenReturn(null); + + org.springframework.test.util.ReflectionTestUtils.invokeMethod(manager, "handleNicEvent", 50L, 10L, true); + verify(dnsZoneNetworkMapDao, times(1)).findByNetworkId(NETWORK_ID); + + when(vmInstanceDao.findByIdIncludingRemoved(10L)).thenReturn(vm); + when(vm.getId()).thenReturn(10L); + when(nicDao.listByVmIdIncludingRemoved(10L)).thenReturn(Collections.singletonList(nic)); + + org.springframework.test.util.ReflectionTestUtils.invokeMethod(manager, "handleVmEvent", 10L, true); + verify(dnsZoneNetworkMapDao, times(2)).findByNetworkId(NETWORK_ID); + } + + @Test + public void testAddDnsServerSuccess() throws Exception { + org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class); + when(callerMock.getType()).thenReturn(Account.Type.ADMIN); + when(cmd.getUrl()).thenReturn("http://newpdns:8081"); + when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS); + when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null); + when(dnsProviderMock.validateAndResolveServer(any())).thenReturn("resolved-id"); + when(dnsServerDao.persist(any())).thenReturn(serverVO); + DnsServer result = manager.addDnsServer(cmd); + assertNotNull(result); + verify(dnsServerDao).persist(any()); + } + + @Test + public void testListDnsServers() { + org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.ListDnsServersCmd.class); + when(domainDao.getDomainParentIds(anyLong())).thenReturn(Collections.emptySet()); + List servers = Collections.singletonList(serverVO); + com.cloud.utils.Pair, Integer> searchPair = new com.cloud.utils.Pair<>(servers, 1); + when(dnsServerDao.searchDnsServer(any(), anyLong(), any(), any(), any(), any())).thenReturn(searchPair); + + DnsServerJoinVO joinVO = mock(DnsServerJoinVO.class); + when(joinVO.getProviderType()).thenReturn(DnsProviderType.PowerDNS.toString()); + when(joinVO.getState()).thenReturn(DnsServer.State.Enabled); + when(dnsServerJoinDao.listByUuids(any())).thenReturn(Collections.singletonList(joinVO)); + + ListResponse res = manager.listDnsServers(cmd); + assertEquals(1, res.getCount().intValue()); + } + + @Test + public void testUpdateDnsServer() throws Exception { + org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class); + when(cmd.getId()).thenReturn(SERVER_ID); + when(cmd.getName()).thenReturn("updated-name"); + when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO); + when(dnsServerDao.update(eq(SERVER_ID), any())).thenReturn(true); + DnsServer res = manager.updateDnsServer(cmd); + assertNotNull(res); + verify(dnsServerDao).update(eq(SERVER_ID), any()); + } + + @Test + public void testListDnsZones() { + org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd cmd = mock(org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd.class); + when(cmd.getId()).thenReturn(null); + when(dnsServerDao.listDnsServerIdsByAccountId(anyLong())).thenReturn(Collections.emptyList()); + List zones = Collections.singletonList(zoneVO); + com.cloud.utils.Pair, Integer> searchPair = new com.cloud.utils.Pair<>(zones, 1); + when(dnsZoneDao.searchZones(any(), anyLong(), any(), any(), any(), any())).thenReturn(searchPair); + + DnsZoneJoinVO joinVO = mock(DnsZoneJoinVO.class); + when(dnsZoneJoinDao.listByUuids(any())).thenReturn(Collections.singletonList(joinVO)); + + ListResponse res = manager.listDnsZones(cmd); + assertEquals(1, res.getCount().intValue()); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java new file mode 100644 index 00000000000..8a622f24899 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderUtilTest.java @@ -0,0 +1,91 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.dns; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class DnsProviderUtilTest { + + @Test + public void testNormalizeDnsRecordValueA() { + String result = DnsProviderUtil.normalizeDnsRecordValue(" 1.2.3.4 ", DnsRecord.RecordType.A); + assertEquals("1.2.3.4", result); + } + + @Test + public void testNormalizeDnsRecordValueAAAA() { + String result = DnsProviderUtil.normalizeDnsRecordValue(" 2001:db8::1 ", DnsRecord.RecordType.AAAA); + assertEquals("2001:db8::1", result); + } + + @Test + public void testNormalizeDnsRecordValueCNAME() { + // Appends dot in the process? No, normalizeDomain trims, lowercases, removes trailing dot, and checks validity. + String result = DnsProviderUtil.normalizeDnsRecordValue(" Host.Example.Com. ", DnsRecord.RecordType.CNAME); + assertEquals("host.example.com", result); + } + + @Test + public void testNormalizeDnsRecordValueNS() { + String result = DnsProviderUtil.normalizeDnsRecordValue("NS1.EXAMPLE.COM", DnsRecord.RecordType.NS); + assertEquals("ns1.example.com", result); + } + + @Test + public void testNormalizeDnsRecordValuePTR() { + String result = DnsProviderUtil.normalizeDnsRecordValue("ptr.valid.zone.", DnsRecord.RecordType.PTR); + assertEquals("ptr.valid.zone", result); + } + + @Test + public void testNormalizeDnsRecordValueSRV() { + String result = DnsProviderUtil.normalizeDnsRecordValue("srv.example.com", DnsRecord.RecordType.SRV); + assertEquals("srv.example.com", result); + } + + @Test + public void testNormalizeDnsRecordValueMX() { + // MX records just get trimmed and lowercased + String result = DnsProviderUtil.normalizeDnsRecordValue(" 10 MAIL.EXAMPLE.COM ", DnsRecord.RecordType.MX); + assertEquals("10 mail.example.com", result); + } + + @Test + public void testNormalizeDnsRecordValueTXT() { + // TXT records are preserved exactly + String result = DnsProviderUtil.normalizeDnsRecordValue(" Exact text value. ", DnsRecord.RecordType.TXT); + assertEquals(" Exact text value. ", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeDnsRecordValueEmpty() { + DnsProviderUtil.normalizeDnsRecordValue(" ", DnsRecord.RecordType.A); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeDnsRecordValueNull() { + DnsProviderUtil.normalizeDnsRecordValue(null, DnsRecord.RecordType.A); + } + + @Test(expected = IllegalArgumentException.class) + public void testNormalizeDnsRecordValueInvalidDomain() { + DnsProviderUtil.normalizeDnsRecordValue("invalid!domain", DnsRecord.RecordType.CNAME); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/dns/dao/DnsServerDaoImplTest.java b/server/src/test/java/org/apache/cloudstack/dns/dao/DnsServerDaoImplTest.java new file mode 100644 index 00000000000..4c5e0925572 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/dns/dao/DnsServerDaoImplTest.java @@ -0,0 +1,117 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.dns.dao; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.cloudstack.dns.DnsProviderType; +import org.apache.cloudstack.dns.DnsServer; +import org.apache.cloudstack.dns.vo.DnsServerVO; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class DnsServerDaoImplTest { + + DnsServerDaoImpl dao; + DnsServerVO mockServer; + + @Before + public void setUp() { + dao = spy(new DnsServerDaoImpl()); + mockServer = new DnsServerVO("test-server", "http://pdns:8081", 8081, "localhost", DnsProviderType.PowerDNS, null, "apikey", false, null, Collections.singletonList("ns1.example.com"), 1L, 10L); + } + + @Test + public void testFindByUrlAndAccount() { + doReturn(mockServer).when(dao).findOneBy(any(SearchCriteria.class)); + + DnsServer result = dao.findByUrlAndAccount("http://pdns:8081", 1L); + assertNotNull(result); + assertEquals("test-server", result.getName()); + assertEquals("http://pdns:8081", result.getUrl()); + } + + @Test + public void testListDnsServerIdsByAccountId() { + List expectedIds = Arrays.asList(100L); + doReturn(expectedIds).when(dao).customSearch(any(SearchCriteria.class), any()); + + List result = dao.listDnsServerIdsByAccountId(1L); + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(100L, result.get(0).longValue()); + } + + @Test + public void testListDnsServerIdsByAccountIdNullAccount() { + List expectedIds = Arrays.asList(100L, 200L); + doReturn(expectedIds).when(dao).customSearch(any(SearchCriteria.class), any()); + + List result = dao.listDnsServerIdsByAccountId(null); + assertNotNull(result); + assertEquals(2, result.size()); + } + + @Test + public void testSearchDnsServerWithAllParams() { + List expected = Collections.singletonList(mockServer); + Pair, Integer> expectedPair = new Pair<>(expected, 1); + doReturn(expectedPair).when(dao).searchAndCount(any(SearchCriteria.class), any()); + + Filter filter = new Filter(DnsServerVO.class, "id", true, 0L, 10L); + Set domainIds = new HashSet<>(Arrays.asList(10L, 20L)); + Pair, Integer> result = dao.searchDnsServer(100L, 1L, domainIds, DnsProviderType.PowerDNS, "test", filter); + + assertNotNull(result); + assertEquals(1, result.first().size()); + assertEquals(1, result.second().intValue()); + assertEquals("test-server", result.first().get(0).getName()); + } + + @Test + public void testSearchDnsServerWithNullParams() { + List expected = Collections.singletonList(mockServer); + Pair, Integer> expectedPair = new Pair<>(expected, 1); + doReturn(expectedPair).when(dao).searchAndCount(any(SearchCriteria.class), any()); + + Filter filter = new Filter(DnsServerVO.class, "id", true, 0L, 10L); + Set domainIds = new HashSet<>(); + Pair, Integer> result = dao.searchDnsServer(null, null, domainIds, null, null, filter); + + assertNotNull(result); + assertEquals(1, result.first().size()); + assertEquals(1, result.second().intValue()); + } +} diff --git a/server/src/test/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImplTest.java b/server/src/test/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImplTest.java new file mode 100644 index 00000000000..817bcd980c4 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/dns/dao/DnsZoneDaoImplTest.java @@ -0,0 +1,111 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.dns.dao; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.cloudstack.dns.DnsZone; +import org.apache.cloudstack.dns.vo.DnsZoneVO; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.utils.Pair; +import com.cloud.utils.db.Filter; +import com.cloud.utils.db.SearchCriteria; + +@RunWith(MockitoJUnitRunner.class) +public class DnsZoneDaoImplTest { + + DnsZoneDaoImpl dao; + DnsZoneVO mockZone; + + @Before + public void setUp() { + dao = spy(new DnsZoneDaoImpl()); + mockZone = new DnsZoneVO("example.com", DnsZone.ZoneType.Public, 1L, 10L, 100L, "test zone"); + } + + @Test + public void testListByAccount() { + List expected = Collections.singletonList(mockZone); + doReturn(expected).when(dao).listBy(any(SearchCriteria.class)); + + List result = dao.listByAccount(10L); + assertEquals(1, result.size()); + assertEquals("example.com", result.get(0).getName()); + } + + @Test + public void testFindByNameServerAndType() { + doReturn(mockZone).when(dao).findOneBy(any(SearchCriteria.class)); + + DnsZoneVO result = dao.findByNameServerAndType("example.com", 1L, DnsZone.ZoneType.Public); + assertNotNull(result); + assertEquals("example.com", result.getName()); + } + + @Test + public void testFindDnsZonesByServerId() { + List expected = Collections.singletonList(mockZone); + doReturn(expected).when(dao).listBy(any(SearchCriteria.class)); + + List result = dao.findDnsZonesByServerId(1L); + assertEquals(1, result.size()); + } + + @Test + public void testSearchZonesWithAllParams() { + List expected = Collections.singletonList(mockZone); + Pair, Integer> expectedPair = new Pair<>(expected, 1); + doReturn(expectedPair).when(dao).searchAndCount(any(SearchCriteria.class), any()); + + Filter filter = new Filter(DnsZoneVO.class, "id", true, 0L, 10L); + List ownDnsServerIds = Arrays.asList(1L, 2L); + Pair, Integer> result = dao.searchZones(1L, 10L, ownDnsServerIds, 1L, "example", filter); + + assertNotNull(result); + assertEquals(1, result.first().size()); + assertEquals(1, result.second().intValue()); + } + + @Test + public void testSearchZonesWithNullParams() { + List expected = Collections.singletonList(mockZone); + Pair, Integer> expectedPair = new Pair<>(expected, 1); + doReturn(expectedPair).when(dao).searchAndCount(any(SearchCriteria.class), any()); + + Filter filter = new Filter(DnsZoneVO.class, "id", true, 0L, 10L); + List ownDnsServerIds = new ArrayList<>(); + Pair, Integer> result = dao.searchZones(null, null, ownDnsServerIds, null, null, filter); + + assertNotNull(result); + assertEquals(1, result.first().size()); + assertEquals(1, result.second().intValue()); + } +}