diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java index 172aa16e5d7..493fb00ebe7 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/VeeamControlServlet.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.veeam; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -59,7 +60,7 @@ public class VeeamControlServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { String method = req.getMethod(); - String path = normalize(req.getPathInfo()); + String path = normalize(req); Negotiation.OutFormat outFormat = Negotiation.responseFormat(req); LOGGER.info("Received {} request for {} with out format: {}", method, path, outFormat); @@ -107,9 +108,15 @@ public class VeeamControlServlet extends HttpServlet { } } - private String normalize(String pathInfo) { - if (pathInfo == null || pathInfo.isBlank()) return "/"; - return pathInfo; + private String normalize(HttpServletRequest req) { + String path = req.getPathInfo(); + if (path == null || path.isBlank()) { + path = req.getRequestURI(); + } + if (path == null || path.isBlank()) { + return "/"; + } + return path; } protected void handleRoot(HttpServletRequest req, HttpServletResponse resp, Negotiation.OutFormat outFormat) @@ -117,13 +124,13 @@ public class VeeamControlServlet extends HttpServlet { String method = req.getMethod(); if (!"GET".equals(method) && !"POST".equals(method)) { - // You didn’t list 405; keep it simple with 400 throw Error.badRequest("Unsupported method for root: " + method); } + Map responseData = new HashMap<>(); + responseData.put("name", VeeamControlService.PLUGIN_NAME); + responseData.put("pluginVersion", this.getClass().getPackage().getImplementationVersion()); - writer.write(resp, 200, Map.of( - "name", VeeamControlService.PLUGIN_NAME, - "pluginVersion", this.getClass().getPackage().getImplementationVersion()), outFormat); + writer.write(resp, 200, responseData, outFormat); } public void methodNotAllowed(final HttpServletResponse resp, final String allow, final Negotiation.OutFormat outFormat) throws IOException { diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java index 4b07f32ee03..7c5a25daea0 100644 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java +++ b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/adapter/ServerAdapter.java @@ -26,17 +26,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import javax.inject.Inject; -import org.apache.cloudstack.acl.Role; -import org.apache.cloudstack.acl.RolePermissionEntity; -import org.apache.cloudstack.acl.RoleService; -import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.acl.Rule; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroupVO; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; @@ -58,7 +52,6 @@ import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; import org.apache.cloudstack.api.command.admin.vm.DeployVMCmdByAdmin; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; import org.apache.cloudstack.api.command.user.job.ListAsyncJobsCmd; -import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd; import org.apache.cloudstack.api.command.user.network.ListNetworksCmd; import org.apache.cloudstack.api.command.user.offering.ListServiceOfferingsCmd; import org.apache.cloudstack.api.command.user.tag.ListTagsCmd; @@ -77,11 +70,8 @@ import org.apache.cloudstack.api.command.user.vmsnapshot.RevertToVMSnapshotCmd; import org.apache.cloudstack.api.command.user.volume.AssignVolumeCmd; import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.DeleteVolumeCmd; import org.apache.cloudstack.api.command.user.volume.DestroyVolumeCmd; -import org.apache.cloudstack.api.command.user.volume.DetachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.ListVolumesCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; import org.apache.cloudstack.api.command.user.volume.UpdateVolumeCmd; import org.apache.cloudstack.api.command.user.zone.ListZonesCmd; import org.apache.cloudstack.api.response.ListResponse; @@ -100,7 +90,6 @@ import org.apache.cloudstack.query.QueryService; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.veeam.VeeamControlService; -import org.apache.cloudstack.veeam.api.TagsRouteHandler; import org.apache.cloudstack.veeam.api.converter.AsyncJobJoinVOToJobConverter; import org.apache.cloudstack.veeam.api.converter.BackupVOToBackupConverter; import org.apache.cloudstack.veeam.api.converter.ClusterVOToClusterConverter; @@ -190,7 +179,6 @@ import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.DomainService; import com.cloud.user.User; -import com.cloud.user.UserAccount; import com.cloud.user.UserDataVO; import com.cloud.user.dao.UserDataDao; import com.cloud.uservm.UserVm; @@ -211,28 +199,7 @@ import com.cloud.vm.dao.VMInstanceDetailsDao; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; -// ToDo: check access for list APIs when not ROOT admin - public class ServerAdapter extends ManagerBase { - private static final String SERVICE_ACCOUNT_NAME = "veemserviceuser"; - private static final String SERVICE_ACCOUNT_ROLE_NAME = "Veeam Service Role"; - private static final String SERVICE_ACCOUNT_FIRST_NAME = "Veeam"; - private static final String SERVICE_ACCOUNT_LAST_NAME = "Service User"; - private static final List> SERVICE_ACCOUNT_ROLE_ALLOWED_APIS = Arrays.asList( - QueryAsyncJobResultCmd.class, - ListVMsCmd.class, - DeployVMCmd.class, - StartVMCmd.class, - StopVMCmd.class, - DestroyVMCmd.class, - ListVolumesCmd.class, - CreateVolumeCmd.class, - DeleteVolumeCmd.class, - AttachVolumeCmd.class, - DetachVolumeCmd.class, - ResizeVolumeCmd.class, - ListNetworksCmd.class - ); private static final List SUPPORTED_STORAGE_TYPES = Arrays.asList( Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, @@ -241,9 +208,6 @@ public class ServerAdapter extends ManagerBase { private static final String VM_TA_KEY = "veeam_tag"; private static final String WORKER_VM_GUEST_CPU_MODE = "host-passthrough"; - @Inject - RoleService roleService; - @Inject AccountService accountService; @@ -346,17 +310,6 @@ public class ServerAdapter extends ManagerBase { @Inject DomainDao domainDao; - protected static Tag getDummyTagByName(String name) { - Tag tag = new Tag(); - String id = UUID.nameUUIDFromBytes(String.format("veeam:%s", name.toLowerCase()).getBytes()).toString(); - tag.setId(id); - tag.setName(name); - tag.setDescription(String.format("Default %s tag", name.toLowerCase())); - tag.setHref(VeeamControlService.ContextPath.value() + TagsRouteHandler.BASE_ROUTE + "/" + id); - tag.setParent(ResourceTagVOToTagConverter.getRootTagRef()); - return tag; - } - protected static Map getDummyTags() { Map tags = new HashMap<>(); Tag rootTag = ResourceTagVOToTagConverter.getRootTag(); @@ -364,56 +317,6 @@ public class ServerAdapter extends ManagerBase { return tags; } - protected Role createServiceAccountRole() { - Role role = roleService.createRole(SERVICE_ACCOUNT_ROLE_NAME, RoleType.User, - SERVICE_ACCOUNT_ROLE_NAME, false); - for (Class allowedApi : SERVICE_ACCOUNT_ROLE_ALLOWED_APIS) { - final String apiName = BaseCmd.getCommandNameByClass(allowedApi); - roleService.createRolePermission(role, new Rule(apiName), RolePermissionEntity.Permission.ALLOW, - String.format("Allow %s", apiName)); - } - roleService.createRolePermission(role, new Rule("*"), RolePermissionEntity.Permission.DENY, - "Deny all"); - logger.debug("Created default role for Veeam service account in projects: {}", role); - return role; - } - - protected Role getServiceAccountRole() { - List roles = roleService.findRolesByName(SERVICE_ACCOUNT_ROLE_NAME); - if (CollectionUtils.isNotEmpty(roles)) { - Role role = roles.get(0); - logger.debug("Found default role for Veeam service account in projects: {}", role); - return role; - } - return createServiceAccountRole(); - } - - protected UserAccount createServiceAccount() { - CallContext.register(User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM); - try { - Role role = getServiceAccountRole(); - UserAccount userAccount = accountService.createUserAccount(SERVICE_ACCOUNT_NAME, - UUID.randomUUID().toString(), SERVICE_ACCOUNT_FIRST_NAME, - SERVICE_ACCOUNT_LAST_NAME, null, null, SERVICE_ACCOUNT_NAME, Account.Type.NORMAL, role.getId(), - 1L, null, null, null, null, User.Source.NATIVE); - logger.debug("Created Veeam service account: {}", userAccount); - return userAccount; - } finally { - CallContext.unregister(); - } - } - - protected Pair getDefaultServiceAccount() { - UserAccount userAccount = accountService.getActiveUserAccount(SERVICE_ACCOUNT_NAME, 1L); - if (userAccount == null) { - userAccount = createServiceAccount(); - } else { - logger.debug("Veeam service user account found: {}", userAccount); - } - return new Pair<>(accountService.getActiveUser(userAccount.getId()), - accountService.getActiveAccountById(userAccount.getAccountId())); - } - protected void waitForJobCompletion(long jobId) { long timeoutNanos = TimeUnit.MINUTES.toNanos(5); final long deadline = System.nanoTime() + timeoutNanos; diff --git a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/BasicAuthFilter.java b/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/BasicAuthFilter.java deleted file mode 100644 index 22f76b8058e..00000000000 --- a/plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/BasicAuthFilter.java +++ /dev/null @@ -1,110 +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.veeam.filter; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.cloudstack.veeam.VeeamControlService; -import org.apache.cloudstack.veeam.VeeamControlServlet; - -public class BasicAuthFilter implements Filter { - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // no-op - } - - @Override - public void destroy() { - // no-op - } - - @Override - public void doFilter( - ServletRequest request, - ServletResponse response, - FilterChain chain - ) throws IOException, ServletException { - - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse resp = (HttpServletResponse) response; - - String expectedUser = VeeamControlService.Username.value(); - String expectedPass = VeeamControlService.Password.value(); - - String auth = req.getHeader("Authorization"); - if (auth == null || !auth.regionMatches(true, 0, "Basic ", 0, 6)) { - unauthorized(resp); - return; - } - - String decoded; - try { - decoded = new String( - Base64.getDecoder().decode(auth.substring(6)), - StandardCharsets.UTF_8 - ); - } catch (IllegalArgumentException e) { - unauthorized(resp); - return; - } - - int idx = decoded.indexOf(':'); - if (idx <= 0) { - unauthorized(resp); - return; - } - - String user = decoded.substring(0, idx); - String pass = decoded.substring(idx + 1); - - if (!constantTimeEquals(user, expectedUser) - || !constantTimeEquals(pass, expectedPass)) { - unauthorized(resp); - return; - } - - chain.doFilter(request, response); - } - - private void unauthorized(HttpServletResponse resp) { - throw VeeamControlServlet.Error.unauthorized("Unauthorized"); - } - - private boolean constantTimeEquals(String a, String b) { - byte[] x = a.getBytes(StandardCharsets.UTF_8); - byte[] y = b.getBytes(StandardCharsets.UTF_8); - if (x.length != y.length) return false; - int r = 0; - for (int i = 0; i < x.length; i++) { - r |= x[i] ^ y[i]; - } - return r == 0; - } -} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServerTest.java new file mode 100644 index 00000000000..6fe38287322 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServerTest.java @@ -0,0 +1,227 @@ +// 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.veeam; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.ConfigKey.Scope; +import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; +import org.apache.cloudstack.utils.server.ServerPropertiesUtil; +import org.eclipse.jetty.server.Server; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +public class VeeamControlServerTest { + + private static final String KEY_ENABLED = "integration.veeam.control.enabled"; + private static final String KEY_PORT = "integration.veeam.control.port"; + private static final String KEY_CONTEXT_PATH = "integration.veeam.control.context.path"; + private static final String KEY_DEVELOPER_LOGS = "integration.veeam.control.developer.logs"; + + private ConfigDepotImpl previousDepot; + private Properties previousServerProperties; + private final Map globalValues = new HashMap<>(); + + @Before + public void setUp() throws Exception { + previousDepot = getConfigDepot(); + final ConfigDepotImpl depot = mock(ConfigDepotImpl.class); + when(depot.getConfigStringValue(Mockito.anyString(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> { + final String key = invocation.getArgument(0); + final Scope scope = invocation.getArgument(1); + if (scope == Scope.Global) { + return globalValues.get(key); + } + return null; + }); + setConfigDepot(depot); + + previousServerProperties = getServerProperties(); + Properties props = new Properties(); + props.setProperty("https.keystore", ""); + props.setProperty("https.keystore.password", ""); + setServerProperties(props); + } + + @After + public void tearDown() throws Exception { + setConfigDepot(previousDepot); + setServerProperties(previousServerProperties); + resetConfigKeyCache(VeeamControlService.Enabled); + resetConfigKeyCache(VeeamControlService.Port); + resetConfigKeyCache(VeeamControlService.ContextPath); + resetConfigKeyCache(VeeamControlService.DeveloperLogs); + } + + @Test + public void testConstructorSortsHandlersByPriorityDescending() throws Exception { + final RouteHandler low = mock(RouteHandler.class); + final RouteHandler high = mock(RouteHandler.class); + when(low.priority()).thenReturn(1); + when(high.priority()).thenReturn(10); + + final VeeamControlServer server = new VeeamControlServer(List.of(low, high), mock(VeeamControlService.class)); + final List handlers = getRouteHandlers(server); + + assertEquals(high, handlers.get(0)); + assertEquals(low, handlers.get(1)); + } + + @Test + public void testStartIfEnabledReturnsWithoutStartingWhenDisabled() throws Exception { + globalValues.put(KEY_ENABLED, "false"); + resetConfigKeyCache(VeeamControlService.Enabled); + + final VeeamControlServer server = new VeeamControlServer(List.of(), mock(VeeamControlService.class)); + server.startIfEnabled(); + + assertNull(getJettyServer(server)); + } + + @Test + public void testStartIfEnabledStartsHttpServerWhenEnabled() throws Exception { + globalValues.put(KEY_ENABLED, "true"); + globalValues.put(KEY_PORT, "0"); + globalValues.put(KEY_CONTEXT_PATH, "/ovirt-engine"); + resetConfigKeyCache(VeeamControlService.Enabled); + resetConfigKeyCache(VeeamControlService.Port); + resetConfigKeyCache(VeeamControlService.ContextPath); + + final VeeamControlService service = mock(VeeamControlService.class); + when(service.getCurrentManagementServerHostId()).thenReturn(1L); + + final VeeamControlServer server = new VeeamControlServer(List.of(), service); + try { + server.startIfEnabled(); + final Server jetty = getJettyServer(server); + assertNotNull(jetty); + assertTrue(jetty.isStarted()); + } finally { + server.stop(); + } + } + + @Test + public void testStopStopsExistingJettyServerAndClearsReference() throws Exception { + final VeeamControlServer server = new VeeamControlServer(List.of(), mock(VeeamControlService.class)); + final Server jetty = mock(Server.class); + setJettyServer(server, jetty); + + server.stop(); + + verify(jetty).stop(); + assertNull(getJettyServer(server)); + } + + @Test + public void testGetRequestResponseMetadataIncludesHeadersWhenDeveloperLogsEnabled() throws Exception { + globalValues.put(KEY_DEVELOPER_LOGS, "true"); + resetConfigKeyCache(VeeamControlService.DeveloperLogs); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api"); + request.setRemoteAddr("127.0.0.1"); + request.setQueryString("x=1"); + request.addHeader("X-Test", "abc"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(202); + + final Method metadataMethod = VeeamControlServer.class.getDeclaredMethod("getRequestResponseMetadata", + HttpServletRequest.class, HttpServletResponse.class); + metadataMethod.setAccessible(true); + final String metadata = (String) metadataMethod.invoke(null, request, response); + + assertTrue(metadata.contains("remote address: 127.0.0.1")); + assertTrue(metadata.contains("uri: /api?x=1")); + assertTrue(metadata.contains("headers: [X-Test=abc;")); + assertTrue(metadata.contains("status: 202")); + } + + @SuppressWarnings("unchecked") + private static List getRouteHandlers(final VeeamControlServer server) throws Exception { + final Field field = VeeamControlServer.class.getDeclaredField("routeHandlers"); + field.setAccessible(true); + return (List) field.get(server); + } + + private static Server getJettyServer(final VeeamControlServer server) throws Exception { + final Field field = VeeamControlServer.class.getDeclaredField("server"); + field.setAccessible(true); + return (Server) field.get(server); + } + + private static void setJettyServer(final VeeamControlServer server, final Server jetty) throws Exception { + final Field field = VeeamControlServer.class.getDeclaredField("server"); + field.setAccessible(true); + field.set(server, jetty); + } + + private static ConfigDepotImpl getConfigDepot() throws Exception { + final Field field = ConfigKey.class.getDeclaredField("s_depot"); + field.setAccessible(true); + return (ConfigDepotImpl) field.get(null); + } + + private static void setConfigDepot(final ConfigDepotImpl depot) throws Exception { + final Field field = ConfigKey.class.getDeclaredField("s_depot"); + field.setAccessible(true); + field.set(null, depot); + } + + @SuppressWarnings("unchecked") + private static Properties getServerProperties() throws Exception { + final Field field = ServerPropertiesUtil.class.getDeclaredField("propertiesRef"); + field.setAccessible(true); + final AtomicReference ref = (AtomicReference) field.get(null); + return ref.get(); + } + + @SuppressWarnings("unchecked") + private static void setServerProperties(final Properties properties) throws Exception { + final Field field = ServerPropertiesUtil.class.getDeclaredField("propertiesRef"); + field.setAccessible(true); + final AtomicReference ref = (AtomicReference) field.get(null); + ref.set(properties); + } + + private static void resetConfigKeyCache(final ConfigKey configKey) throws Exception { + final Field valueField = ConfigKey.class.getDeclaredField("_value"); + valueField.setAccessible(true); + valueField.set(configKey, null); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java index 4ae0808238b..7c50d10136d 100644 --- a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServiceImplTest.java @@ -16,25 +16,170 @@ // under the License. package org.apache.cloudstack.veeam; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.ConfigKey.Scope; +import org.apache.cloudstack.framework.config.impl.ConfigDepotImpl; import org.apache.cloudstack.veeam.api.dto.ImageTransfer; import org.apache.cloudstack.veeam.utils.Mapper; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import com.fasterxml.jackson.core.JsonProcessingException; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.utils.UuidUtils; @RunWith(MockitoJUnitRunner.class) public class VeeamControlServiceImplTest { + private static final String KEY_ENABLED = "integration.veeam.control.enabled"; + private static final String KEY_PORT = "integration.veeam.control.port"; + private static final String KEY_USERNAME = "integration.veeam.control.api.username"; + private static final String KEY_PASSWORD = "integration.veeam.control.api.password"; + private static final String KEY_ALLOWED_CIDRS = "integration.veeam.control.allowed.client.cidrs"; + + private ConfigDepotImpl previousDepot; + private final Map globalValues = new HashMap<>(); + + @Before + public void setUp() throws Exception { + previousDepot = getConfigDepot(); + final ConfigDepotImpl depot = mock(ConfigDepotImpl.class); + when(depot.getConfigStringValue(Mockito.anyString(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> { + final String key = invocation.getArgument(0); + final Scope scope = invocation.getArgument(1); + if (scope == Scope.Global) { + return globalValues.get(key); + } + return null; + }); + setConfigDepot(depot); + } + + @After + public void tearDown() throws Exception { + setConfigDepot(previousDepot); + resetConfigKeyCache(VeeamControlService.Enabled); + resetConfigKeyCache(VeeamControlService.Port); + resetConfigKeyCache(VeeamControlService.Username); + resetConfigKeyCache(VeeamControlService.Password); + resetConfigKeyCache(VeeamControlService.AllowedClientCidrs); + } + @Test - public void test_parseImageTransfer() { + public void testParseImageTransfer() { String data = "{\"active\":false,\"direction\":\"upload\",\"format\":\"cow\",\"inactivity_timeout\":3600,\"phase\":\"cancelled\",\"shallow\":false,\"transferred\":0,\"link\":[],\"disk\":{\"id\":\"dba4d72d-01de-4267-aa8e-305996b53599\"},\"image\":{},\"backup\":{\"creation_date\":0}}"; Mapper mapper = new Mapper(); try { - ImageTransfer request = mapper.jsonMapper().readValue(data, ImageTransfer.class); + mapper.jsonMapper().readValue(data, ImageTransfer.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } + + @Test + public void testGetAllowedClientCidrsInternalSanitizesAndFiltersInvalidEntries() throws Exception { + globalValues.put(KEY_ALLOWED_CIDRS, " 10.0.0.0/24,invalid-cidr, ,192.168.1.100/32 "); + resetConfigKeyCache(VeeamControlService.AllowedClientCidrs); + final VeeamControlServiceImpl service = new VeeamControlServiceImpl(); + + final List cidrs = service.getAllowedClientCidrsInternal(); + + assertEquals(List.of("10.0.0.0/24", "192.168.1.100/32"), cidrs); + } + + @Test + public void testGetAllowedClientCidrsInternalReturnsEmptyListForBlankValue() throws Exception { + globalValues.put(KEY_ALLOWED_CIDRS, " "); + resetConfigKeyCache(VeeamControlService.AllowedClientCidrs); + final VeeamControlServiceImpl service = new VeeamControlServiceImpl(); + + assertTrue(service.getAllowedClientCidrsInternal().isEmpty()); + } + + @Test + public void testValidateCredentials() throws Exception { + globalValues.put(KEY_USERNAME, "veeam-user"); + globalValues.put(KEY_PASSWORD, "veeam-pass"); + resetConfigKeyCache(VeeamControlService.Username); + resetConfigKeyCache(VeeamControlService.Password); + final VeeamControlServiceImpl service = new VeeamControlServiceImpl(); + + assertTrue(service.validateCredentials("veeam-user", "veeam-pass")); + assertFalse(service.validateCredentials("veeam-user", "wrong")); + assertFalse(service.validateCredentials("wrong", "veeam-pass")); + } + + @Test + public void testGetInstanceIdReturnsSystemAccountUuid() { + final VeeamControlServiceImpl service = new VeeamControlServiceImpl(); + final AccountService accountService = mock(AccountService.class); + final Account account = mock(Account.class); + service.accountService = accountService; + when(accountService.getSystemAccount()).thenReturn(account); + when(account.getUuid()).thenReturn("system-account-uuid"); + + assertEquals("system-account-uuid", service.getInstanceId()); + } + + @Test + public void testGetHmacSecretUsesConfiguredInputs() throws Exception { + globalValues.put(KEY_PORT, "8095"); + globalValues.put(KEY_USERNAME, "api-user"); + globalValues.put(KEY_PASSWORD, "api-pass"); + resetConfigKeyCache(VeeamControlService.Port); + resetConfigKeyCache(VeeamControlService.Username); + resetConfigKeyCache(VeeamControlService.Password); + + final VeeamControlServiceImpl service = Mockito.spy(new VeeamControlServiceImpl()); + Mockito.doReturn("instance-uuid").when(service).getInstanceId(); + + final String expected = UuidUtils.nameUUIDFromBytes( + "instance-uuid:8095:api-userapi-pass".getBytes(StandardCharsets.UTF_8)).toString(); + assertEquals(expected, service.getHmacSecret()); + } + + @Test + public void testGetConfigKeysContainsExpectedEntries() { + final VeeamControlServiceImpl service = new VeeamControlServiceImpl(); + + final ConfigKey[] keys = service.getConfigKeys(); + + assertEquals(10, keys.length); + assertEquals(KEY_ENABLED, keys[0].key()); + } + + private static ConfigDepotImpl getConfigDepot() throws Exception { + final Field field = ConfigKey.class.getDeclaredField("s_depot"); + field.setAccessible(true); + return (ConfigDepotImpl) field.get(null); + } + + private static void setConfigDepot(final ConfigDepotImpl depot) throws Exception { + final Field field = ConfigKey.class.getDeclaredField("s_depot"); + field.setAccessible(true); + field.set(null, depot); + } + + private static void resetConfigKeyCache(final ConfigKey configKey) throws Exception { + final Field valueField = ConfigKey.class.getDeclaredField("_value"); + valueField.setAccessible(true); + valueField.set(configKey, null); + } } diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServletTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServletTest.java new file mode 100644 index 00000000000..c788613e52c --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/VeeamControlServletTest.java @@ -0,0 +1,121 @@ +// 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.veeam; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +public class VeeamControlServletTest { + + @Test + public void testServiceHandlesRootRequestForGet() throws Exception { + final VeeamControlServlet servlet = new VeeamControlServlet(Collections.emptyList()); + final HttpServletRequest request = new MockHttpServletRequest("GET", "/"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + servlet.service(request, response); + + assertEquals(200, response.getStatus()); + assertTrue(response.getContentAsString().contains("CloudStack Veeam Control Service")); + } + + @Test + public void testServiceReturnsBadRequestForUnsupportedRootMethod() throws Exception { + final VeeamControlServlet servlet = new VeeamControlServlet(Collections.emptyList()); + final HttpServletRequest request = new MockHttpServletRequest("PUT", "/"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + servlet.service(request, response); + + assertEquals(400, response.getStatus()); + assertTrue(response.getContentAsString().contains("Unsupported method for root")); + } + + @Test + public void testServiceDelegatesToMatchingRouteHandler() throws Exception { + final RouteHandler handler = mock(RouteHandler.class); + final String path = "/api/path"; + final VeeamControlServlet servlet = new VeeamControlServlet(Collections.singletonList(handler)); + final HttpServletRequest request = new MockHttpServletRequest("GET", path); + final MockHttpServletResponse response = new MockHttpServletResponse(); + when(handler.canHandle("GET", path)).thenReturn(true); + + servlet.service(request, response); + + verify(handler).handle(request, response, path, Negotiation.OutFormat.XML, servlet); + } + + @Test + public void testServiceReturnsNotFoundWhenNoHandlerMatches() throws Exception { + final RouteHandler handler = mock(RouteHandler.class); + final String path = "/api/path"; + final VeeamControlServlet servlet = new VeeamControlServlet(Collections.singletonList(handler)); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", path); + request.setPathInfo(path); + request.addHeader("Accept", "application/json"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + when(handler.canHandle("GET", path)).thenReturn(false); + + servlet.service(request, response); + + assertEquals(404, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"reason\":\"Not found\"")); + verify(handler).canHandle("GET", path); + } + + @Test + public void testServiceConvertsHandlerErrorToFaultResponse() throws Exception { + final RouteHandler handler = mock(RouteHandler.class); + final String path = "/api/faultpath"; + final VeeamControlServlet servlet = new VeeamControlServlet(Collections.singletonList(handler)); + final HttpServletRequest request = new MockHttpServletRequest("GET", path); + final MockHttpServletResponse response = new MockHttpServletResponse(); + when(handler.canHandle("GET", path)).thenReturn(true); + doThrow(VeeamControlServlet.Error.unauthorized("denied")).when(handler) + .handle(request, response, path, Negotiation.OutFormat.XML, servlet); + + servlet.service(request, response); + + assertEquals(401, response.getStatus()); + assertTrue(response.getContentAsString().contains("denied")); + } + + @Test + public void testMethodNotAllowedWritesAllowHeaderAndFault() throws Exception { + final VeeamControlServlet servlet = new VeeamControlServlet(Collections.emptyList()); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + servlet.methodNotAllowed(response, "GET, POST", Negotiation.OutFormat.JSON); + + assertEquals(405, response.getStatus()); + assertEquals("GET, POST", response.getHeader("Allow")); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ApiAccessInterceptorTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ApiAccessInterceptorTest.java new file mode 100644 index 00000000000..16fafb9034b --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ApiAccessInterceptorTest.java @@ -0,0 +1,204 @@ +// 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.veeam.adapter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.command.user.zone.ListZonesCmd; +import org.apache.cloudstack.context.CallContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.User; +import com.cloud.utils.Pair; + +public class ApiAccessInterceptorTest { + + private static final long BASE_USER_ID = 100L; + private static final long BASE_ACCOUNT_ID = 200L; + private static final long SERVICE_USER_ID = 300L; + private static final long SERVICE_ACCOUNT_ID = 400L; + + private final ApiAccessInterceptor interceptor = new ApiAccessInterceptor(); + private final AccountManager accountManager = mock(AccountManager.class); + + @Before + public void setUp() { + interceptor.accountManager = accountManager; + CallContext.unregisterAll(); + } + + @After + public void tearDown() { + CallContext.unregisterAll(); + } + + @Test + public void testInvokePassesThroughWhenTargetIsNull() throws Throwable { + final MethodInvocation invocation = mock(MethodInvocation.class); + when(invocation.getThis()).thenReturn(null); + when(invocation.proceed()).thenReturn("ok"); + + final Object result = interceptor.invoke(invocation); + + assertEquals("ok", result); + verify(invocation).proceed(); + verifyNoInteractions(accountManager); + } + + @Test + public void testInvokePassesThroughWhenMethodHasNoApiAccessAnnotation() throws Throwable { + final TestServerAdapter adapter = new TestServerAdapter(serviceUserAccount()); + final MethodInvocation invocation = mock(MethodInvocation.class); + when(invocation.getThis()).thenReturn(adapter); + when(invocation.getMethod()).thenReturn(TestServerAdapter.class.getMethod("noApiAccess")); + when(invocation.proceed()).thenReturn("done"); + + final Object result = interceptor.invoke(invocation); + + assertEquals("done", result); + verify(invocation).proceed(); + verifyNoInteractions(accountManager); + } + + @Test + public void testInvokeChecksApiAccessForDirectlyAnnotatedMethodAndRestoresCallContext() throws Throwable { + registerBaseContext(); + + final TestServerAdapter adapter = new TestServerAdapter(serviceUserAccount()); + final MethodInvocation invocation = mock(MethodInvocation.class); + when(invocation.getThis()).thenReturn(adapter); + when(invocation.getMethod()).thenReturn(TestServerAdapter.class.getMethod("classAnnotated")); + when(invocation.proceed()).thenAnswer(i -> { + assertEquals(SERVICE_USER_ID, CallContext.current().getCallingUserId()); + assertEquals(SERVICE_ACCOUNT_ID, CallContext.current().getCallingAccountId()); + return "secured"; + }); + + final Object result = interceptor.invoke(invocation); + + assertEquals("secured", result); + verify(accountManager).checkApiAccess(adapter.getServiceAccount().second(), + BaseCmd.getCommandNameByClass(ListZonesCmd.class)); + assertEquals(BASE_USER_ID, CallContext.current().getCallingUserId()); + assertEquals(BASE_ACCOUNT_ID, CallContext.current().getCallingAccountId()); + } + + @Test + public void testInvokeFindsAnnotationOnImplementationWhenInterfaceMethodIsUnannotated() throws Throwable { + registerBaseContext(); + + final TestServerAdapter adapter = new TestServerAdapter(serviceUserAccount()); + final Method interfaceMethod = ApiContract.class.getMethod("implAnnotatedThroughInterface"); + final MethodInvocation invocation = mock(MethodInvocation.class); + when(invocation.getThis()).thenReturn(adapter); + when(invocation.getMethod()).thenReturn(interfaceMethod); + when(invocation.proceed()).thenReturn("ok"); + + final Object result = interceptor.invoke(invocation); + + assertEquals("ok", result); + verify(accountManager).checkApiAccess(adapter.getServiceAccount().second(), + BaseCmd.getCommandNameByClass(ListZonesCmd.class)); + assertEquals(BASE_USER_ID, CallContext.current().getCallingUserId()); + assertEquals(BASE_ACCOUNT_ID, CallContext.current().getCallingAccountId()); + } + + @Test + public void testInvokeUnregistersServiceContextWhenProceedThrows() throws Throwable { + registerBaseContext(); + + final TestServerAdapter adapter = new TestServerAdapter(serviceUserAccount()); + final MethodInvocation invocation = mock(MethodInvocation.class); + final RuntimeException expected = new RuntimeException("boom"); + when(invocation.getThis()).thenReturn(adapter); + when(invocation.getMethod()).thenReturn(TestServerAdapter.class.getMethod("classAnnotated")); + when(invocation.proceed()).thenThrow(expected); + + try { + interceptor.invoke(invocation); + } catch (RuntimeException e) { + assertSame(expected, e); + } + + verify(accountManager).checkApiAccess(adapter.getServiceAccount().second(), + BaseCmd.getCommandNameByClass(ListZonesCmd.class)); + assertEquals(BASE_USER_ID, CallContext.current().getCallingUserId()); + assertEquals(BASE_ACCOUNT_ID, CallContext.current().getCallingAccountId()); + } + + private static void registerBaseContext() { + final User baseUser = mock(User.class); + final Account baseAccount = mock(Account.class); + when(baseUser.getId()).thenReturn(BASE_USER_ID); + when(baseAccount.getId()).thenReturn(BASE_ACCOUNT_ID); + CallContext.register(baseUser, baseAccount); + } + + private static Pair serviceUserAccount() { + final User serviceUser = mock(User.class); + final Account serviceAccount = mock(Account.class); + when(serviceUser.getId()).thenReturn(SERVICE_USER_ID); + when(serviceAccount.getId()).thenReturn(SERVICE_ACCOUNT_ID); + return new Pair<>(serviceUser, serviceAccount); + } + + private interface ApiContract { + String implAnnotatedThroughInterface(); + } + + private static class TestServerAdapter extends ServerAdapter implements ApiContract { + private final Pair serviceAccount; + + private TestServerAdapter(final Pair serviceAccount) { + this.serviceAccount = serviceAccount; + } + + @Override + public Pair getServiceAccount() { + return serviceAccount; + } + + @ApiAccess(command = ListZonesCmd.class) + public String classAnnotated() { + return "classAnnotated"; + } + + @Override + @ApiAccess(command = ListZonesCmd.class) + public String implAnnotatedThroughInterface() { + return "implAnnotatedThroughInterface"; + } + + public String noApiAccess() { + return "noApiAccess"; + } + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java new file mode 100644 index 00000000000..bfa407ba49f --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/adapter/ServerAdapterTest.java @@ -0,0 +1,972 @@ +// 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.veeam.adapter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.backup.BackupVO; +import org.apache.cloudstack.backup.dao.BackupDao; +import org.apache.cloudstack.backup.dao.ImageTransferDao; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.jobs.dao.AsyncJobDao; +import org.apache.cloudstack.framework.jobs.impl.AsyncJobVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.veeam.api.dto.DataCenter; +import org.apache.cloudstack.veeam.api.dto.Disk; +import org.apache.cloudstack.veeam.api.dto.Tag; +import org.apache.cloudstack.veeam.api.dto.Vm; +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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.api.query.dao.AsyncJobJoinDao; +import com.cloud.api.query.dao.DataCenterJoinDao; +import com.cloud.api.query.dao.HostJoinDao; +import com.cloud.api.query.dao.StoragePoolJoinDao; +import com.cloud.api.query.dao.UserVmJoinDao; +import com.cloud.api.query.dao.VolumeJoinDao; +import com.cloud.api.query.vo.AsyncJobJoinVO; +import com.cloud.api.query.vo.DataCenterJoinVO; +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.api.query.vo.StoragePoolJoinVO; +import com.cloud.api.query.vo.VolumeJoinVO; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.dao.ClusterDao; +import com.cloud.dc.dao.DataCenterDao; +import com.cloud.domain.dao.DomainDao; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.network.NetworkModel; +import com.cloud.network.Networks; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; +import com.cloud.projects.Project; +import com.cloud.projects.ProjectManager; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.Storage; +import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.user.Account; +import com.cloud.user.AccountService; +import com.cloud.user.User; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.UserVmManager; +import com.cloud.vm.UserVmVO; +import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.snapshot.dao.VMSnapshotDao; + +@RunWith(MockitoJUnitRunner.class) +public class ServerAdapterTest { + + @InjectMocks + ServerAdapter serverAdapter; + + @Mock AccountService accountService; + @Mock DataCenterDao dataCenterDao; + @Mock DataCenterJoinDao dataCenterJoinDao; + @Mock StoragePoolJoinDao storagePoolJoinDao; + @Mock ClusterDao clusterDao; + @Mock HostJoinDao hostJoinDao; + @Mock NetworkDao networkDao; + @Mock UserVmDao userVmDao; + @Mock UserVmJoinDao userVmJoinDao; + @Mock VolumeDao volumeDao; + @Mock VolumeJoinDao volumeJoinDao; + // kept minimal: only mocks used directly by tests + @Mock com.cloud.storage.VolumeApiService volumeApiService; + @Mock PrimaryDataStoreDao primaryDataStoreDao; + @Mock ImageTransferDao imageTransferDao; + @Mock ServiceOfferingDao serviceOfferingDao; + @Mock VMTemplateDao templateDao; + @Mock UserVmManager userVmManager; + @Mock AsyncJobDao asyncJobDao; + @Mock AsyncJobJoinDao asyncJobJoinDao; + @Mock VMSnapshotDao vmSnapshotDao; + @Mock BackupDao backupDao; + @Mock NetworkModel networkModel; + @Mock ProjectManager projectManager; + @Mock DomainDao domainDao; + + @Before + public void setupCallContext() { + CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class)); + } + + @After + public void cleanupCallContext() { + CallContext.unregister(); + } + + + + @Test + public void testGetProvisionedSizeInGb_ExactlyOneGB() { + long gb = 1024L * 1024L * 1024L; + assertEquals(1L, ServerAdapter.getProvisionedSizeInGb(String.valueOf(gb))); + } + + @Test + public void testGetProvisionedSizeInGb_MultipleGB() { + long gb = 1024L * 1024L * 1024L; + assertEquals(5L, ServerAdapter.getProvisionedSizeInGb(String.valueOf(5 * gb))); + } + + @Test + public void testGetProvisionedSizeInGb_LessThanOneGB_RoundsUpToOne() { + assertEquals(1L, ServerAdapter.getProvisionedSizeInGb("512")); + } + + @Test + public void testGetProvisionedSizeInGb_NotExactGB_RoundsUp() { + long gb = 1024L * 1024L * 1024L; + assertEquals(2L, ServerAdapter.getProvisionedSizeInGb(String.valueOf(gb + gb / 2))); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetProvisionedSizeInGb_InvalidString_Throws() { + ServerAdapter.getProvisionedSizeInGb("not-a-number"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetProvisionedSizeInGb_Zero_Throws() { + ServerAdapter.getProvisionedSizeInGb("0"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testGetProvisionedSizeInGb_Negative_Throws() { + ServerAdapter.getProvisionedSizeInGb("-1073741824"); + } + + + @Test + public void testGetDetailsForInstanceCreation_WithUserdata_AddsCpuMode() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + when(offering.isCustomized()).thenReturn(false); + + Map result = ServerAdapter.getDetailsForInstanceCreation("#!/bin/bash", offering, null); + + assertEquals("host-passthrough", result.get(VmDetailConstants.GUEST_CPU_MODE)); + } + + @Test + public void testGetDetailsForInstanceCreation_NoUserdata_NoCpuMode() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + when(offering.isCustomized()).thenReturn(false); + + Map result = ServerAdapter.getDetailsForInstanceCreation(null, offering, null); + + assertFalse(result.containsKey(VmDetailConstants.GUEST_CPU_MODE)); + } + + @Test + public void testGetDetailsForInstanceCreation_CustomizedOffering_AddsDetails() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + when(offering.isCustomized()).thenReturn(true); + when(offering.getCpu()).thenReturn(4); + when(offering.getRamSize()).thenReturn(2048); + when(offering.getSpeed()).thenReturn(null); + + Map result = ServerAdapter.getDetailsForInstanceCreation(null, offering, null); + + assertEquals("4", result.get(VmDetailConstants.CPU_NUMBER)); + assertEquals("2048", result.get(VmDetailConstants.MEMORY)); + assertEquals("1000", result.get(VmDetailConstants.CPU_SPEED)); + } + + @Test + public void testGetDetailsForInstanceCreation_CustomizedOffering_WithSpeed_DoesNotAddDefaultCpuSpeed() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + when(offering.isCustomized()).thenReturn(true); + when(offering.getCpu()).thenReturn(2); + when(offering.getRamSize()).thenReturn(1024); + when(offering.getSpeed()).thenReturn(2000); + + Map result = ServerAdapter.getDetailsForInstanceCreation(null, offering, null); + + assertFalse(result.containsKey(VmDetailConstants.CPU_SPEED)); + } + + @Test + public void testGetDetailsForInstanceCreation_SkipsBiosAndUefiKeys() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + when(offering.isCustomized()).thenReturn(false); + + Map existingDetails = new HashMap<>(); + existingDetails.put("BIOS", "bios_value"); + existingDetails.put("UEFI", "uefi_value"); + existingDetails.put("custom_key", "custom_value"); + + Map result = ServerAdapter.getDetailsForInstanceCreation(null, offering, existingDetails); + + assertFalse(result.containsKey("BIOS")); + assertFalse(result.containsKey("UEFI")); + assertEquals("custom_value", result.get("custom_key")); + } + + @Test + public void testGetDetailsForInstanceCreation_PreservesExistingCpuSpeed() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + when(offering.isCustomized()).thenReturn(true); + when(offering.getCpu()).thenReturn(2); + when(offering.getRamSize()).thenReturn(1024); + when(offering.getSpeed()).thenReturn(null); + + Map existingDetails = new HashMap<>(); + existingDetails.put(VmDetailConstants.CPU_SPEED, "3000"); + + Map result = ServerAdapter.getDetailsForInstanceCreation(null, offering, existingDetails); + + assertEquals("3000", result.get(VmDetailConstants.CPU_SPEED)); + } + + + @Test + public void testGetDummyTags_ContainsRootTag() { + Map tags = ServerAdapter.getDummyTags(); + assertNotNull(tags); + assertFalse(tags.isEmpty()); + } + + + @Test + public void testGetTemplateForInstanceCreation_NullUuid_ReturnsNull() { + assertNull(serverAdapter.getTemplateForInstanceCreation(null)); + } + + @Test + public void testGetTemplateForInstanceCreation_BlankUuid_ReturnsNull() { + assertNull(serverAdapter.getTemplateForInstanceCreation(" ")); + } + + @Test + public void testGetTemplateForInstanceCreation_TemplateNotFound_ReturnsNull() { + when(templateDao.findByUuid("missing-uuid")).thenReturn(null); + assertNull(serverAdapter.getTemplateForInstanceCreation("missing-uuid")); + } + + @Test + public void testGetTemplateForInstanceCreation_TemplateFound_ReturnsTemplate() { + VMTemplateVO template = mock(VMTemplateVO.class); + when(templateDao.findByUuid("valid-uuid")).thenReturn(template); + assertEquals(template, serverAdapter.getTemplateForInstanceCreation("valid-uuid")); + } + + + @Test + public void testGetZoneById_NullId_ReturnsNull() { + assertNull(serverAdapter.getZoneById(null)); + } + + @Test + public void testGetZoneById_ReturnsVoFromDao() { + DataCenterJoinVO vo = mock(DataCenterJoinVO.class); + when(dataCenterJoinDao.findById(1L)).thenReturn(vo); + assertEquals(vo, serverAdapter.getZoneById(1L)); + } + + @Test + public void testGetHostById_NullId_ReturnsNull() { + assertNull(serverAdapter.getHostById(null)); + } + + @Test + public void testGetHostById_ReturnsVoFromDao() { + HostJoinVO vo = mock(HostJoinVO.class); + when(hostJoinDao.findById(2L)).thenReturn(vo); + assertEquals(vo, serverAdapter.getHostById(2L)); + } + + @Test + public void testGetVolumeById_NullId_ReturnsNull() { + assertNull(serverAdapter.getVolumeById(null)); + } + + @Test + public void testGetVolumeById_ReturnsVoFromDao() { + VolumeJoinVO vo = mock(VolumeJoinVO.class); + when(volumeJoinDao.findById(3L)).thenReturn(vo); + assertEquals(vo, serverAdapter.getVolumeById(3L)); + } + + @Test + public void testGetNetworkById_NullId_ReturnsNull() { + assertNull(serverAdapter.getNetworkById(null)); + } + + @Test + public void testGetNetworkById_ReturnsVoFromDao() { + NetworkVO vo = mock(NetworkVO.class); + when(networkDao.findById(4L)).thenReturn(vo); + assertEquals(vo, serverAdapter.getNetworkById(4L)); + } + + + @Test + public void testWaitForJobCompletion_JobNotFound_Returns() { + when(asyncJobDao.findById(99L)).thenReturn(null); + serverAdapter.waitForJobCompletion(99L); + verify(asyncJobDao).findById(99L); + } + + @Test + public void testWaitForJobCompletion_JobAlreadySucceeded_Returns() { + AsyncJobVO job = mock(AsyncJobVO.class); + when(job.getStatus()).thenReturn(AsyncJobVO.Status.SUCCEEDED); + when(asyncJobDao.findById(1L)).thenReturn(job); + serverAdapter.waitForJobCompletion(1L); + } + + @Test + public void testWaitForJobCompletion_JobAlreadyFailed_Returns() { + AsyncJobVO job = mock(AsyncJobVO.class); + when(job.getStatus()).thenReturn(AsyncJobVO.Status.FAILED); + when(asyncJobDao.findById(2L)).thenReturn(job); + serverAdapter.waitForJobCompletion(2L); + } + + + @Test + public void testWaitForJobCompletion_NullJobJoinVO_Returns() { + AsyncJobJoinVO job = null; + serverAdapter.waitForJobCompletion(job); + } + + @Test + public void testWaitForJobCompletion_CompletedJobJoinVO_DelegatesById() { + AsyncJobJoinVO jobVO = mock(AsyncJobJoinVO.class); + when(jobVO.getStatus()).thenReturn(AsyncJobVO.Status.SUCCEEDED.ordinal()); + when(jobVO.getId()).thenReturn(5L); + + AsyncJobVO job = mock(AsyncJobVO.class); + when(job.getStatus()).thenReturn(AsyncJobVO.Status.SUCCEEDED); + when(asyncJobDao.findById(5L)).thenReturn(job); + + serverAdapter.waitForJobCompletion(jobVO); + + verify(asyncJobDao).findById(5L); + } + + + @Test + public void testGetOwnerDetailsForInstanceCreation_NullAccount_ReturnsAllNulls() { + Ternary result = serverAdapter.getOwnerDetailsForInstanceCreation(null); + assertNull(result.first()); + assertNull(result.second()); + assertNull(result.third()); + } + + @Test + public void testGetOwnerDetailsForInstanceCreation_NormalAccount_ReturnsDomainAndName() { + Account account = mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + when(account.getDomainId()).thenReturn(2L); + when(account.getAccountName()).thenReturn("myaccount"); + + Ternary result = serverAdapter.getOwnerDetailsForInstanceCreation(account); + + assertEquals(Long.valueOf(2L), result.first()); + assertEquals("myaccount", result.second()); + assertNull(result.third()); + } + + @Test + public void testGetOwnerDetailsForInstanceCreation_ProjectAccount_ProjectNotFound_ReturnsAllNulls() { + Account account = mock(Account.class); + when(account.getType()).thenReturn(Account.Type.PROJECT); + when(account.getId()).thenReturn(10L); + when(projectManager.findByProjectAccountId(10L)).thenReturn(null); + + Ternary result = serverAdapter.getOwnerDetailsForInstanceCreation(account); + + assertNull(result.first()); + assertNull(result.second()); + assertNull(result.third()); + } + + @Test + public void testGetOwnerDetailsForInstanceCreation_ProjectAccount_ProjectFound_ReturnsProjectId() { + Account account = mock(Account.class); + when(account.getType()).thenReturn(Account.Type.PROJECT); + when(account.getId()).thenReturn(10L); + when(account.getDomainId()).thenReturn(1L); + + Project project = mock(Project.class); + when(project.getId()).thenReturn(5L); + when(projectManager.findByProjectAccountId(10L)).thenReturn(project); + + Ternary result = serverAdapter.getOwnerDetailsForInstanceCreation(account); + + assertEquals(Long.valueOf(1L), result.first()); + assertNull(result.second()); + assertEquals(Long.valueOf(5L), result.third()); + } + + + @Test + public void testGetServiceOfferingFromRequest_BlankUuid_ReturnsNull() { + assertNull(serverAdapter.getServiceOfferingFromRequest(null, null, "", 2, 1024)); + assertNull(serverAdapter.getServiceOfferingFromRequest(null, null, null, 2, 1024)); + } + + @Test + public void testGetServiceOfferingFromRequest_OfferingNotFound_ReturnsNull() { + when(serviceOfferingDao.findByUuid("uuid1")).thenReturn(null); + assertNull(serverAdapter.getServiceOfferingFromRequest(null, null, "uuid1", 2, 1024)); + } + + @Test + public void testGetServiceOfferingFromRequest_AccessDenied_ReturnsNull() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + Account account = mock(Account.class); + when(serviceOfferingDao.findByUuid("uuid2")).thenReturn(offering); + doThrow(new PermissionDeniedException("denied")) + .when(accountService).checkAccess(eq(account), eq(offering), any()); + + assertNull(serverAdapter.getServiceOfferingFromRequest(null, account, "uuid2", 2, 1024)); + } + + @Test + public void testGetServiceOfferingFromRequest_NotCustomized_CpuMemoryMismatch_ReturnsNull() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + Account account = mock(Account.class); + when(serviceOfferingDao.findByUuid("uuid3")).thenReturn(offering); + doNothing().when(accountService).checkAccess(eq(account), eq(offering), any()); + when(offering.isCustomized()).thenReturn(false); + when(offering.getCpu()).thenReturn(4); + + assertNull(serverAdapter.getServiceOfferingFromRequest(null, account, "uuid3", 2, 1024)); + } + + @Test + public void testGetServiceOfferingFromRequest_NotCustomized_CpuMemoryMatch_ReturnsOffering() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + Account account = mock(Account.class); + when(serviceOfferingDao.findByUuid("uuid4")).thenReturn(offering); + doNothing().when(accountService).checkAccess(eq(account), eq(offering), any()); + when(offering.isCustomized()).thenReturn(false); + when(offering.getCpu()).thenReturn(2); + when(offering.getRamSize()).thenReturn(1024); + + assertEquals(offering, serverAdapter.getServiceOfferingFromRequest(null, account, "uuid4", 2, 1024)); + } + + @Test + public void testGetServiceOfferingFromRequest_Customized_ValidParams_SetsAndReturnsOffering() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + Account account = mock(Account.class); + when(serviceOfferingDao.findByUuid("uuid5")).thenReturn(offering); + doNothing().when(accountService).checkAccess(eq(account), eq(offering), any()); + when(offering.isCustomized()).thenReturn(true); + doNothing().when(userVmManager).validateCustomParameters(eq(offering), any()); + + ServiceOfferingVO result = serverAdapter.getServiceOfferingFromRequest(null, account, "uuid5", 2, 1024); + + assertEquals(offering, result); + verify(offering).setCpu(2); + verify(offering).setRamSize(1024); + } + + @Test + public void testGetServiceOfferingFromRequest_Customized_InvalidParams_ReturnsNull() { + ServiceOfferingVO offering = mock(ServiceOfferingVO.class); + Account account = mock(Account.class); + when(serviceOfferingDao.findByUuid("uuid6")).thenReturn(offering); + doNothing().when(accountService).checkAccess(eq(account), eq(offering), any()); + when(offering.isCustomized()).thenReturn(true); + doThrow(new InvalidParameterValueException("invalid")) + .when(userVmManager).validateCustomParameters(eq(offering), any()); + + assertNull(serverAdapter.getServiceOfferingFromRequest(null, account, "uuid6", 2, 1024)); + } + + + @Test + public void testAccountCannotAccessNetwork_CanAccess_ReturnsFalse() { + NetworkVO network = mock(NetworkVO.class); + Account account = mock(Account.class); + when(accountService.getActiveAccountById(1L)).thenReturn(account); + doNothing().when(networkModel).checkNetworkPermissions(account, network); + + assertFalse(serverAdapter.accountCannotAccessNetwork(network, 1L)); + } + + @Test + public void testAccountCannotAccessNetwork_CannotAccess_ReturnsTrue() { + NetworkVO network = mock(NetworkVO.class); + Account account = mock(Account.class); + when(accountService.getActiveAccountById(1L)).thenReturn(account); + doThrow(new CloudRuntimeException("Access denied")) + .when(networkModel).checkNetworkPermissions(account, network); + + assertTrue(serverAdapter.accountCannotAccessNetwork(network, 1L)); + } + + + @Test + public void testValidateInstanceStorage_SupportedStorageType_NoException() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(1L); + VolumeVO vol = mock(VolumeVO.class); + when(vol.getPoolId()).thenReturn(10L); + when(volumeDao.findUsableVolumesForInstance(1L)).thenReturn(List.of(vol)); + StoragePoolVO pool = mock(StoragePoolVO.class); + when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + when(primaryDataStoreDao.listByIds(anyList())).thenReturn(List.of(pool)); + + serverAdapter.validateInstanceStorage(vm); + } + + @Test(expected = InvalidParameterValueException.class) + public void testValidateInstanceStorage_UnsupportedStorageType_Throws() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(2L); + VolumeVO vol = mock(VolumeVO.class); + when(vol.getPoolId()).thenReturn(20L); + when(volumeDao.findUsableVolumesForInstance(2L)).thenReturn(List.of(vol)); + StoragePoolVO pool = mock(StoragePoolVO.class); + when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); + when(pool.getName()).thenReturn("ceph-pool"); + when(primaryDataStoreDao.listByIds(anyList())).thenReturn(List.of(pool)); + + serverAdapter.validateInstanceStorage(vm); + } + + @Test + public void testValidateInstanceStorage_NoVolumes_NoException() { + UserVmVO vm = mock(UserVmVO.class); + when(vm.getId()).thenReturn(3L); + when(volumeDao.findUsableVolumesForInstance(3L)).thenReturn(Collections.emptyList()); + when(primaryDataStoreDao.listByIds(anyList())).thenReturn(Collections.emptyList()); + + serverAdapter.validateInstanceStorage(vm); + } + + + @Test + public void testGetBackupDisks_NullVolumeInfos_ReturnsEmptyList() { + BackupVO backup = mock(BackupVO.class); + when(backup.getBackedUpVolumes()).thenReturn(null); + + assertTrue(serverAdapter.getBackupDisks(backup).isEmpty()); + } + + @Test + public void testGetBackupDisks_EmptyVolumeInfos_ReturnsEmptyList() { + BackupVO backup = mock(BackupVO.class); + when(backup.getBackedUpVolumes()).thenReturn(Collections.emptyList()); + + assertTrue(serverAdapter.getBackupDisks(backup).isEmpty()); + } + + + @Test + public void testGetResourceOwnerFiltersWithDomainIds_NullDomainPath_ReturnsNullDomainIds() { + ServerAdapter spyAdapter = spy(serverAdapter); + doReturn(new Pair<>(List.of(1L, 2L), (String) null)).when(spyAdapter).getResourceOwnerFilters(); + + Pair, List> result = spyAdapter.getResourceOwnerFiltersWithDomainIds(); + + assertEquals(List.of(1L, 2L), result.first()); + assertNull(result.second()); + } + + @Test + public void testGetResourceOwnerFiltersWithDomainIds_WithDomainPath_ReturnsDomainIds() { + ServerAdapter spyAdapter = spy(serverAdapter); + doReturn(new Pair<>(List.of(1L), "ROOT/subdomain")).when(spyAdapter).getResourceOwnerFilters(); + when(domainDao.getDomainChildrenIds("ROOT/subdomain")).thenReturn(List.of(10L, 11L)); + + Pair, List> result = spyAdapter.getResourceOwnerFiltersWithDomainIds(); + + assertEquals(List.of(10L, 11L), result.second()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetDataCenter_NotFound_Throws() { + when(dataCenterJoinDao.findByUuid("dc-uuid")).thenReturn(null); + serverAdapter.getDataCenter("dc-uuid"); + } + + @Test + public void testGetDataCenter_Found_ReturnsDataCenter() { + DataCenterJoinVO vo = mock(DataCenterJoinVO.class); + when(dataCenterJoinDao.findByUuid("dc-uuid")).thenReturn(vo); + + DataCenter result = serverAdapter.getDataCenter("dc-uuid"); + + assertNotNull(result); + } + + + @Test + public void testListAllDataCenters_ReturnsConvertedList() { + DataCenterJoinVO vo = mock(DataCenterJoinVO.class); + when(dataCenterJoinDao.listAll(any())).thenReturn(List.of(vo)); + + List result = serverAdapter.listAllDataCenters(0L, 10L); + + assertNotNull(result); + assertEquals(1, result.size()); + } + + @Test + public void testListAllDataCenters_EmptyList_ReturnsEmpty() { + when(dataCenterJoinDao.listAll(any())).thenReturn(Collections.emptyList()); + + assertTrue(serverAdapter.listAllDataCenters(0L, 10L).isEmpty()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListStorageDomainsByDcId_DataCenterNotFound_Throws() { + when(dataCenterDao.findByUuid("dc-uuid")).thenReturn(null); + serverAdapter.listStorageDomainsByDcId("dc-uuid", 0L, 10L); + } + + @Test + public void testListStorageDomainsByDcId_Found_ReturnsList() { + DataCenterVO dcVO = mock(DataCenterVO.class); + when(dcVO.getId()).thenReturn(1L); + when(dataCenterDao.findByUuid("dc-uuid")).thenReturn(dcVO); + StoragePoolJoinVO poolVO = mock(StoragePoolJoinVO.class); + when(storagePoolJoinDao.listByZoneAndType(eq(1L), any(), any())).thenReturn(List.of(poolVO)); + + assertNotNull(serverAdapter.listStorageDomainsByDcId("dc-uuid", 0L, 10L)); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListNetworksByDcId_DataCenterNotFound_Throws() { + when(dataCenterJoinDao.findByUuid("dc-uuid")).thenReturn(null); + serverAdapter.listNetworksByDcId("dc-uuid", 0L, 10L); + } + + @Test + public void testListNetworksByDcId_Found_ReturnsEmptyListWhenNoNetworks() { + DataCenterJoinVO dcVO = mock(DataCenterJoinVO.class); + when(dcVO.getId()).thenReturn(1L); + when(dataCenterJoinDao.findByUuid("dc-uuid")).thenReturn(dcVO); + when(networkDao.listByZoneAndTrafficType(eq(1L), eq(Networks.TrafficType.Guest), any())) + .thenReturn(Collections.emptyList()); + assertTrue(serverAdapter.listNetworksByDcId("dc-uuid", 0L, 10L).isEmpty()); + } + + + @Test + public void testListAllClusters_ReturnsEmptyListWhenNoClusters() { + when(clusterDao.listByHypervisorType(any(), any())).thenReturn(Collections.emptyList()); + assertTrue(serverAdapter.listAllClusters(0L, 10L).isEmpty()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetCluster_NotFound_Throws() { + when(clusterDao.findByUuid("cl-uuid")).thenReturn(null); + serverAdapter.getCluster("cl-uuid"); + } + + + @Test + public void testListAllHosts_ReturnsList() { + HostJoinVO hostVO = mock(HostJoinVO.class); + when(hostJoinDao.listRoutingHostsByHypervisor(any(), any())).thenReturn(List.of(hostVO)); + + assertEquals(1, serverAdapter.listAllHosts(0L, 10L).size()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetHost_NotFound_Throws() { + when(hostJoinDao.findByUuid("host-uuid")).thenReturn(null); + serverAdapter.getHost("host-uuid"); + } + + @Test + public void testGetHost_Found_ReturnsHost() { + HostJoinVO vo = mock(HostJoinVO.class); + when(hostJoinDao.findByUuid("host-uuid")).thenReturn(vo); + + assertNotNull(serverAdapter.getHost("host-uuid")); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetVnicProfile_NotFound_Throws() { + when(networkDao.findByUuid("net-uuid")).thenReturn(null); + serverAdapter.getVnicProfile("net-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetInstance_NotFound_Throws() { + when(userVmJoinDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.getInstance("vm-uuid", false, false, false, false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteInstance_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.deleteInstance("vm-uuid", false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testStartInstance_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.startInstance("vm-uuid", false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testStopInstance_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.stopInstance("vm-uuid", false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testShutdownInstance_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.shutdownInstance("vm-uuid", false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetDisk_NotFound_Throws() { + when(volumeDao.findByUuid("vol-uuid")).thenReturn(null); + serverAdapter.getDisk("vol-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteDisk_NotFound_Throws() { + when(volumeDao.findByUuid("vol-uuid")).thenReturn(null); + serverAdapter.deleteDisk("vol-uuid"); + } + + @Test + public void testDeleteDisk_Found_DeletesVolume() { + VolumeVO vo = mock(VolumeVO.class); + when(vo.getId()).thenReturn(10L); + when(volumeDao.findByUuid("vol-uuid")).thenReturn(vo); + Account sysAccount = mock(Account.class); + when(accountService.getSystemAccount()).thenReturn(sysAccount); + + serverAdapter.deleteDisk("vol-uuid"); + + verify(volumeApiService).deleteVolume(10L, sysAccount); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testCopyDisk_AlwaysThrows() { + serverAdapter.copyDisk("any-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testReduceDisk_AlwaysThrows() { + serverAdapter.reduceDisk("any-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListNicsByInstanceUuid_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.listNicsByInstanceUuid("vm-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListDiskAttachmentsByInstanceUuid_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.listDiskAttachmentsByInstanceUuid("vm-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListSnapshotsByInstanceUuid_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.listSnapshotsByInstanceUuid("vm-uuid"); + } + + @Test + public void testListSnapshotsByInstanceUuid_Found_ReturnsEmptyList() { + UserVmVO vmVO = mock(UserVmVO.class); + when(vmVO.getId()).thenReturn(1L); + when(vmVO.getUuid()).thenReturn("vm-uuid"); + when(userVmDao.findByUuid("vm-uuid")).thenReturn(vmVO); + when(vmSnapshotDao.findByVm(1L)).thenReturn(Collections.emptyList()); + + assertTrue(serverAdapter.listSnapshotsByInstanceUuid("vm-uuid").isEmpty()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetSnapshot_NotFound_Throws() { + when(vmSnapshotDao.findByUuid("snap-uuid")).thenReturn(null); + serverAdapter.getSnapshot("snap-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteSnapshot_NotFound_Throws() { + when(vmSnapshotDao.findByUuid("snap-uuid")).thenReturn(null); + serverAdapter.deleteSnapshot("snap-uuid", false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testRevertInstanceToSnapshot_NotFound_Throws() { + when(vmSnapshotDao.findByUuid("snap-uuid")).thenReturn(null); + serverAdapter.revertInstanceToSnapshot("snap-uuid", false); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListBackupsByInstanceUuid_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.listBackupsByInstanceUuid("vm-uuid"); + } + + @Test + public void testListBackupsByInstanceUuid_Found_ReturnsEmptyList() { + UserVmVO vmVO = mock(UserVmVO.class); + when(vmVO.getId()).thenReturn(1L); + when(userVmDao.findByUuid("vm-uuid")).thenReturn(vmVO); + when(backupDao.searchByVmIds(anyList())).thenReturn(Collections.emptyList()); + + assertTrue(serverAdapter.listBackupsByInstanceUuid("vm-uuid").isEmpty()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetBackup_NotFound_Throws() { + when(backupDao.findByUuidIncludingRemoved("backup-uuid")).thenReturn(null); + serverAdapter.getBackup("backup-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testFinalizeBackup_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.finalizeBackup("vm-uuid", "backup-uuid"); + } + + @Test(expected = InvalidParameterValueException.class) + public void testFinalizeBackup_BackupNotFound_Throws() { + UserVmVO vmVO = mock(UserVmVO.class); + when(userVmDao.findByUuid("vm-uuid")).thenReturn(vmVO); + when(backupDao.findByUuid("backup-uuid")).thenReturn(null); + serverAdapter.finalizeBackup("vm-uuid", "backup-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListCheckpointsByInstanceUuid_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.listCheckpointsByInstanceUuid("vm-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testDeleteCheckpoint_VmNotFound_Throws() { + when(userVmDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.deleteCheckpoint("vm-uuid", "chk-001"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetJob_NotFound_Throws() { + when(asyncJobJoinDao.findByUuidIncludingRemoved("job-uuid")).thenReturn(null); + serverAdapter.getJob("job-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testGetImageTransfer_NotFound_Throws() { + when(imageTransferDao.findByUuidIncludingRemoved("transfer-uuid")).thenReturn(null); + serverAdapter.getImageTransfer("transfer-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testCancelImageTransfer_NotFound_Throws() { + when(imageTransferDao.findByUuid("transfer-uuid")).thenReturn(null); + serverAdapter.cancelImageTransfer("transfer-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testFinalizeImageTransfer_NotFound_Throws() { + when(imageTransferDao.findByUuid("transfer-uuid")).thenReturn(null); + serverAdapter.finalizeImageTransfer("transfer-uuid"); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateInstance_VmNotFound_Throws() { + when(userVmJoinDao.findByUuid("vm-uuid")).thenReturn(null); + serverAdapter.updateInstance("vm-uuid", new Vm()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testUpdateDisk_NotFound_Throws() { + when(volumeDao.findByUuid("vol-uuid")).thenReturn(null); + serverAdapter.updateDisk("vol-uuid", new Disk()); + } + + + @Test(expected = InvalidParameterValueException.class) + public void testListDisksByBackupUuid_AlwaysThrows() { + serverAdapter.listDisksByBackupUuid("backup-uuid"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ApiRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ApiRouteHandlerTest.java new file mode 100644 index 00000000000..4fe63c4d11d --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ApiRouteHandlerTest.java @@ -0,0 +1,92 @@ +// 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.veeam.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.cloud.user.Account; +import com.cloud.user.User; +import com.cloud.utils.Pair; +import org.apache.cloudstack.veeam.VeeamControlService; +import org.apache.cloudstack.veeam.api.dto.Api; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class ApiRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testCanHandleAndHandleRootApiRequest() throws Exception { + final ApiRouteHandler handler = new ApiRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + handler.veeamControlService = mock(VeeamControlService.class); + + final User user = mock(User.class); + when(user.getUuid()).thenReturn("user-1"); + when(handler.serverAdapter.getServiceAccount()).thenReturn(new Pair<>(user, mock(Account.class))); + when(handler.veeamControlService.getInstanceId()).thenReturn("instance-1"); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api", Negotiation.OutFormat.JSON, newServlet()); + + assertTrue(handler.canHandle("GET", "/api?x=1")); + verify(response.response).setStatus(200); + assertContains(response.body(), "\"instance_id\":\"instance-1\""); + assertContains(response.body(), "\"authenticated_user\""); + assertContains(response.body(), "clusters/search"); + } + + @Test + public void testCreateApiObjectBuildsLinksAndUserReferences() { + final ApiRouteHandler handler = new ApiRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + handler.veeamControlService = mock(VeeamControlService.class); + + final User user = mock(User.class); + when(user.getUuid()).thenReturn("service-user"); + when(handler.serverAdapter.getServiceAccount()).thenReturn(new Pair<>(user, mock(Account.class))); + when(handler.veeamControlService.getInstanceId()).thenReturn("instance-2"); + + final Api api = handler.createApiObject("/ctx/api"); + + assertNotNull(api.getLink()); + assertTrue(!api.getLink().isEmpty()); + assertEquals("instance-2", api.getProductInfo().getInstanceId()); + assertEquals("service-user", api.getAuthenticatedUser().getId()); + assertEquals("service-user", api.getEffectiveUser().getId()); + assertNotNull(api.getTime()); + } + + @Test + public void testHandleUnknownPathReturnsNotFound() throws Exception { + final ApiRouteHandler handler = new ApiRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + handler.veeamControlService = mock(VeeamControlService.class); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/unknown", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setStatus(404); + assertContains(response.body(), "\"reason\":\"Not found\""); + } +} + diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ClustersRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ClustersRouteHandlerTest.java new file mode 100644 index 00000000000..a7ee380f137 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ClustersRouteHandlerTest.java @@ -0,0 +1,80 @@ +// 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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.Cluster; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class ClustersRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListReturnsNamedClusterList() throws Exception { + final ClustersRouteHandler handler = new ClustersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllClusters(null, 25L)).thenReturn(List.of(withId(new Cluster(), "cl-1"))); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET", Map.of("max", "25"), null, null), response.response, + "/api/clusters", Negotiation.OutFormat.JSON, newServlet()); + + verify(handler.serverAdapter).listAllClusters(null, 25L); + verify(response.response).setStatus(200); + assertContains(response.body(), "\"cluster\":["); + assertContains(response.body(), "\"id\":\"cl-1\""); + } + + @Test + public void testHandleGetByIdReturnsClusterAndMissingClusterIsNotFound() throws Exception { + final ClustersRouteHandler handler = new ClustersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getCluster("cl-1")).thenReturn(withId(new Cluster(), "cl-1")); + when(handler.serverAdapter.getCluster("missing")).thenThrow(new InvalidParameterValueException("missing")); + + final ResponseCapture ok = newResponse(); + handler.handle(newRequest("GET"), ok.response, "/api/clusters/cl-1", Negotiation.OutFormat.JSON, newServlet()); + verify(ok.response).setStatus(200); + assertContains(ok.body(), "\"id\":\"cl-1\""); + + final ResponseCapture missing = newResponse(); + handler.handle(newRequest("GET"), missing.response, "/api/clusters/missing", Negotiation.OutFormat.JSON, newServlet()); + verify(missing.response).setStatus(404); + assertContains(missing.body(), "missing"); + } + + @Test + public void testHandleRejectsUnsupportedMethod() throws Exception { + final ClustersRouteHandler handler = new ClustersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("POST"), response.response, "/api/clusters", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setHeader("Allow", "GET"); + verify(response.response).setStatus(405); + assertContains(response.body(), "Method Not Allowed"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandlerTest.java new file mode 100644 index 00000000000..38ddbfad37b --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/DataCentersRouteHandlerTest.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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.DataCenter; +import org.apache.cloudstack.veeam.api.dto.Network; +import org.apache.cloudstack.veeam.api.dto.StorageDomain; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class DataCentersRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndById() throws Exception { + final DataCentersRouteHandler handler = new DataCentersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllDataCenters(null, 20L)).thenReturn(List.of(withId(new DataCenter(), "dc-1"))); + when(handler.serverAdapter.getDataCenter("dc-1")).thenReturn(withId(new DataCenter(), "dc-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET", Map.of("max", "20"), null, null), list.response, + "/api/datacenters", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"data_center\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/datacenters/dc-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"dc-1\""); + } + + @Test + public void testHandleGetStorageDomainsAndNetworksByDataCenterId() throws Exception { + final DataCentersRouteHandler handler = new DataCentersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listStorageDomainsByDcId("dc-1", null, 15L)) + .thenReturn(List.of(withId(new StorageDomain(), "sd-1"))); + when(handler.serverAdapter.listNetworksByDcId("dc-1", null, 15L)) + .thenReturn(List.of(withId(new Network(), "net-1"))); + + final ResponseCapture storageDomains = newResponse(); + handler.handle(newRequest("GET", Map.of("max", "15"), null, null), storageDomains.response, + "/api/datacenters/dc-1/storagedomains", Negotiation.OutFormat.JSON, newServlet()); + verify(storageDomains.response).setStatus(200); + assertContains(storageDomains.body(), "\"storage_domain\":["); + assertContains(storageDomains.body(), "sd-1"); + + final ResponseCapture networks = newResponse(); + handler.handle(newRequest("GET", Map.of("max", "15"), null, null), networks.response, + "/api/datacenters/dc-1/networks", Negotiation.OutFormat.JSON, newServlet()); + verify(networks.response).setStatus(200); + assertContains(networks.body(), "\"network\":["); + assertContains(networks.body(), "net-1"); + } + + @Test + public void testHandleMissingDataCenterReturnsNotFound() throws Exception { + final DataCentersRouteHandler handler = new DataCentersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getDataCenter("missing")).thenThrow(new InvalidParameterValueException("missing dc")); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/datacenters/missing", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setStatus(404); + assertContains(response.body(), "missing dc"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/DisksRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/DisksRouteHandlerTest.java new file mode 100644 index 00000000000..f3872d0cd89 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/DisksRouteHandlerTest.java @@ -0,0 +1,122 @@ +// 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.veeam.api; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.veeam.api.dto.Disk; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class DisksRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndGetById() throws Exception { + final DisksRouteHandler handler = new DisksRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllDisks(null, 10L)).thenReturn(List.of(withId(new Disk(), "disk-1"))); + when(handler.serverAdapter.getDisk("disk-1")).thenReturn(withId(new Disk(), "disk-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET", Map.of("max", "10"), null, null), list.response, + "/api/disks", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"disk\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/disks/disk-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"disk-1\""); + } + + @Test + public void testHandlePostAndPutParseDiskJson() throws Exception { + final DisksRouteHandler handler = new DisksRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + + final ArgumentCaptor createCaptor = ArgumentCaptor.forClass(Disk.class); + final Disk created = withId(new Disk(), "disk-created"); + created.setName("created-disk"); + when(handler.serverAdapter.createDisk(createCaptor.capture())).thenReturn(created); + + final ResponseCapture post = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"name\":\"created-disk\"}"), post.response, + "/api/disks", Negotiation.OutFormat.JSON, newServlet()); + verify(post.response).setStatus(201); + assertEquals("created-disk", createCaptor.getValue().getName()); + assertContains(post.body(), "disk-created"); + + final ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Disk.class); + final Disk updated = withId(new Disk(), "disk-1"); + updated.setName("updated-disk"); + when(handler.serverAdapter.updateDisk(org.mockito.ArgumentMatchers.eq("disk-1"), updateCaptor.capture())).thenReturn(updated); + + final ResponseCapture put = newResponse(); + handler.handle(newRequest("PUT", Map.of(), "application/json", "{\"name\":\"updated-disk\"}"), put.response, + "/api/disks/disk-1", Negotiation.OutFormat.JSON, newServlet()); + verify(put.response).setStatus(200); + assertEquals("updated-disk", updateCaptor.getValue().getName()); + assertContains(put.body(), "updated-disk"); + } + + @Test + public void testHandleDeleteCopyAndReduceRoutes() throws Exception { + final DisksRouteHandler handler = new DisksRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.copyDisk("disk-1")).thenReturn(withId(new Disk(), "copy-1")); + when(handler.serverAdapter.reduceDisk("disk-1")).thenReturn(withId(new Disk(), "reduced-1")); + + final ResponseCapture delete = newResponse(); + handler.handle(newRequest("DELETE"), delete.response, "/api/disks/disk-1", Negotiation.OutFormat.JSON, newServlet()); + verify(handler.serverAdapter).deleteDisk("disk-1"); + verify(delete.response).setStatus(200); + assertContains(delete.body(), "Deleted disk ID: disk-1"); + + final ResponseCapture copy = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{}"), copy.response, + "/api/disks/disk-1/copy", Negotiation.OutFormat.JSON, newServlet()); + verify(copy.response).setStatus(200); + assertContains(copy.body(), "copy-1"); + + final ResponseCapture reduce = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{}"), reduce.response, + "/api/disks/disk-1/reduce", Negotiation.OutFormat.JSON, newServlet()); + verify(reduce.response).setStatus(200); + assertContains(reduce.body(), "reduced-1"); + } + + @Test + public void testHandleCopyRejectsUnsupportedMethod() throws Exception { + final DisksRouteHandler handler = new DisksRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/disks/disk-1/copy", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setHeader("Allow", "POST"); + verify(response.response).setStatus(405); + assertContains(response.body(), "Method Not Allowed"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/HostsRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/HostsRouteHandlerTest.java new file mode 100644 index 00000000000..ef937037472 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/HostsRouteHandlerTest.java @@ -0,0 +1,64 @@ +// 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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.Host; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class HostsRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndGetById() throws Exception { + final HostsRouteHandler handler = new HostsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllHosts(null, null)).thenReturn(List.of(withId(new Host(), "host-1"))); + when(handler.serverAdapter.getHost("host-1")).thenReturn(withId(new Host(), "host-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/hosts", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"host\":["); + assertContains(list.body(), "host-1"); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/hosts/host-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"host-1\""); + } + + @Test + public void testHandleGetByIdNotFound() throws Exception { + final HostsRouteHandler handler = new HostsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getHost("missing")).thenThrow(new InvalidParameterValueException("missing host")); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/hosts/missing", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setStatus(404); + assertContains(response.body(), "missing host"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandlerTest.java new file mode 100644 index 00000000000..7ad85a4a4ee --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/ImageTransfersRouteHandlerTest.java @@ -0,0 +1,102 @@ +// 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.veeam.api; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.ImageTransfer; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class ImageTransfersRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndById() throws Exception { + final ImageTransfersRouteHandler handler = new ImageTransfersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllImageTransfers(null, 5L)).thenReturn(List.of(withId(new ImageTransfer(), "transfer-1"))); + when(handler.serverAdapter.getImageTransfer("transfer-1")).thenReturn(withId(new ImageTransfer(), "transfer-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET", Map.of("max", "5"), null, null), list.response, + "/api/imagetransfers", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"image_transfer\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/imagetransfers/transfer-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"transfer-1\""); + } + + @Test + public void testHandlePostParsesRequestAndCancelFinalizeActions() throws Exception { + final ImageTransfersRouteHandler handler = new ImageTransfersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(ImageTransfer.class); + final ImageTransfer created = withId(new ImageTransfer(), "transfer-created"); + created.setPhase("transferring"); + when(handler.serverAdapter.createImageTransfer(captor.capture())).thenReturn(created); + + final ResponseCapture post = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"phase\":\"transferring\"}"), post.response, + "/api/imagetransfers", Negotiation.OutFormat.JSON, newServlet()); + verify(post.response).setStatus(201); + assertEquals("transferring", captor.getValue().getPhase()); + assertContains(post.body(), "transfer-created"); + + final ResponseCapture cancel = newResponse(); + handler.handle(newRequest("POST"), cancel.response, "/api/imagetransfers/transfer-1/cancel", Negotiation.OutFormat.JSON, newServlet()); + verify(handler.serverAdapter).cancelImageTransfer("transfer-1"); + verify(cancel.response).setStatus(200); + assertContains(cancel.body(), "cancelled successfully"); + + final ResponseCapture finalize = newResponse(); + handler.handle(newRequest("POST"), finalize.response, "/api/imagetransfers/transfer-1/finalize", Negotiation.OutFormat.JSON, newServlet()); + verify(handler.serverAdapter).finalizeImageTransfer("transfer-1"); + verify(finalize.response).setStatus(200); + assertContains(finalize.body(), "finalized successfully"); + } + + @Test + public void testHandleMissingTransferAndUnsupportedActionMethod() throws Exception { + final ImageTransfersRouteHandler handler = new ImageTransfersRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getImageTransfer("missing")).thenThrow(new InvalidParameterValueException("missing transfer")); + + final ResponseCapture missing = newResponse(); + handler.handle(newRequest("GET"), missing.response, "/api/imagetransfers/missing", Negotiation.OutFormat.JSON, newServlet()); + verify(missing.response).setStatus(404); + assertContains(missing.body(), "missing transfer"); + + final ResponseCapture wrongMethod = newResponse(); + handler.handle(newRequest("GET"), wrongMethod.response, "/api/imagetransfers/transfer-1/cancel", Negotiation.OutFormat.JSON, newServlet()); + verify(wrongMethod.response).setHeader("Allow", "POST"); + verify(wrongMethod.response).setStatus(405); + assertContains(wrongMethod.body(), "Method Not Allowed"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/JobsRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/JobsRouteHandlerTest.java new file mode 100644 index 00000000000..f3236896d4b --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/JobsRouteHandlerTest.java @@ -0,0 +1,67 @@ +// 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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.Job; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class JobsRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndGetById() throws Exception { + final JobsRouteHandler handler = new JobsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listPendingJobs()).thenReturn(List.of(withId(new Job(), "job-1"))); + when(handler.serverAdapter.getJob("job-1")).thenReturn(withId(new Job(), "job-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/jobs", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"job\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/jobs/job-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"job-1\""); + } + + @Test + public void testHandleMissingJobAndUnsupportedMethod() throws Exception { + final JobsRouteHandler handler = new JobsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getJob("missing")).thenThrow(new InvalidParameterValueException("missing job")); + + final ResponseCapture missing = newResponse(); + handler.handle(newRequest("GET"), missing.response, "/api/jobs/missing", Negotiation.OutFormat.JSON, newServlet()); + verify(missing.response).setStatus(404); + assertContains(missing.body(), "missing job"); + + final ResponseCapture wrongMethod = newResponse(); + handler.handle(newRequest("POST"), wrongMethod.response, "/api/jobs", Negotiation.OutFormat.JSON, newServlet()); + verify(wrongMethod.response).setStatus(405); + assertContains(wrongMethod.body(), "Method Not Allowed"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/NetworksRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/NetworksRouteHandlerTest.java new file mode 100644 index 00000000000..35381b673c4 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/NetworksRouteHandlerTest.java @@ -0,0 +1,63 @@ +// 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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.Network; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class NetworksRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndById() throws Exception { + final NetworksRouteHandler handler = new NetworksRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllNetworks(null, null)).thenReturn(List.of(withId(new Network(), "net-1"))); + when(handler.serverAdapter.getNetwork("net-1")).thenReturn(withId(new Network(), "net-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/networks", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"network\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/networks/net-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"net-1\""); + } + + @Test + public void testHandleMissingNetworkReturnsNotFound() throws Exception { + final NetworksRouteHandler handler = new NetworksRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getNetwork("missing")).thenThrow(new InvalidParameterValueException("missing network")); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/networks/missing", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setStatus(404); + assertContains(response.body(), "missing network"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/RouteHandlerTestSupport.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/RouteHandlerTestSupport.java new file mode 100644 index 00000000000..ff0a6d82223 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/RouteHandlerTestSupport.java @@ -0,0 +1,102 @@ +// 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.veeam.api; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.cloudstack.veeam.VeeamControlServlet; +import org.apache.cloudstack.veeam.api.dto.BaseDto; + +class RouteHandlerTestSupport { + + protected VeeamControlServlet newServlet() { + return new VeeamControlServlet(Collections.emptyList()); + } + + protected HttpServletRequest newRequest(final String method) throws Exception { + return newRequest(method, Collections.emptyMap(), null, null); + } + + protected HttpServletRequest newRequest(final String method, final Map params, + final String contentType, final String body) throws Exception { + final HttpServletRequest request = mock(HttpServletRequest.class); + final Map safeParams = params == null ? Collections.emptyMap() : params; + + when(request.getMethod()).thenReturn(method); + when(request.getContentType()).thenReturn(contentType); + when(request.getParameterMap()).thenReturn(toParameterMap(safeParams)); + when(request.getParameter(org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> safeParams.get(invocation.getArgument(0))); + when(request.getReader()).thenReturn(new BufferedReader(new StringReader(body == null ? "" : body))); + return request; + } + + protected ResponseCapture newResponse() throws Exception { + final HttpServletResponse response = mock(HttpServletResponse.class); + final StringWriter sink = new StringWriter(); + final PrintWriter writer = new PrintWriter(sink); + when(response.getWriter()).thenReturn(writer); + return new ResponseCapture(response, sink, writer); + } + + protected static T withId(final T dto, final String id) { + dto.setId(id); + dto.setHref("/api/test/" + id); + return dto; + } + + protected static void assertContains(final String actual, final String expected) { + assertTrue("Expected body to contain: " + expected + " but was: " + actual, actual.contains(expected)); + } + + private static Map toParameterMap(final Map params) { + final java.util.LinkedHashMap result = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : params.entrySet()) { + result.put(entry.getKey(), new String[]{entry.getValue()}); + } + return result; + } + + protected static class ResponseCapture { + final HttpServletResponse response; + private final StringWriter sink; + private final PrintWriter writer; + + ResponseCapture(final HttpServletResponse response, final StringWriter sink, final PrintWriter writer) { + this.response = response; + this.sink = sink; + this.writer = writer; + } + + String body() { + writer.flush(); + return sink.toString(); + } + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/TagsRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/TagsRouteHandlerTest.java new file mode 100644 index 00000000000..1b01887aabf --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/TagsRouteHandlerTest.java @@ -0,0 +1,63 @@ +// 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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.Tag; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class TagsRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndById() throws Exception { + final TagsRouteHandler handler = new TagsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllTags(null, null)).thenReturn(List.of(withId(new Tag(), "tag-1"))); + when(handler.serverAdapter.getTag("tag-1")).thenReturn(withId(new Tag(), "tag-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/tags", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"tag\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/tags/tag-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"tag-1\""); + } + + @Test + public void testHandleMissingTagReturnsNotFound() throws Exception { + final TagsRouteHandler handler = new TagsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getTag("missing")).thenThrow(new InvalidParameterValueException("missing tag")); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/tags/missing", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setStatus(404); + assertContains(response.body(), "missing tag"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/VmsRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/VmsRouteHandlerTest.java new file mode 100644 index 00000000000..4262096d6e3 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/VmsRouteHandlerTest.java @@ -0,0 +1,297 @@ +// 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.veeam.api; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.veeam.api.dto.Backup; +import org.apache.cloudstack.veeam.api.dto.Checkpoint; +import org.apache.cloudstack.veeam.api.dto.Disk; +import org.apache.cloudstack.veeam.api.dto.DiskAttachment; +import org.apache.cloudstack.veeam.api.dto.Nic; +import org.apache.cloudstack.veeam.api.dto.ResourceAction; +import org.apache.cloudstack.veeam.api.dto.Snapshot; +import org.apache.cloudstack.veeam.api.dto.Vm; +import org.apache.cloudstack.veeam.api.dto.VmAction; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class VmsRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListUsesFollowFlags() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllInstances(true, true, true, true, null, 10L)) + .thenReturn(List.of(withId(new Vm(), "vm-1"))); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET", Map.of( + "max", "10", + "all_content", "true", + "follow", "tags,disk_attachments.disk,nics.reporteddevices"), null, null), + response.response, "/api/vms", Negotiation.OutFormat.JSON, newServlet()); + + verify(handler.serverAdapter).listAllInstances(true, true, true, true, null, 10L); + verify(response.response).setStatus(200); + assertContains(response.body(), "\"vm\":["); + assertContains(response.body(), "vm-1"); + } + + @Test + public void testHandlePostAndUpdateParseVmJson() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + + final ArgumentCaptor createCaptor = ArgumentCaptor.forClass(Vm.class); + final Vm created = withId(new Vm(), "vm-created"); + created.setName("vm-created"); + when(handler.serverAdapter.createInstance(createCaptor.capture())).thenReturn(created); + + final ResponseCapture post = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"name\":\"vm-created\"}"), post.response, + "/api/vms", Negotiation.OutFormat.JSON, newServlet()); + verify(post.response).setStatus(201); + assertEquals("vm-created", createCaptor.getValue().getName()); + assertContains(post.body(), "vm-created"); + + final ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Vm.class); + final Vm updated = withId(new Vm(), "vm-1"); + updated.setName("vm-updated"); + when(handler.serverAdapter.updateInstance(eq("vm-1"), updateCaptor.capture())).thenReturn(updated); + + final ResponseCapture put = newResponse(); + handler.handle(newRequest("PUT", Map.of(), "application/json", "{\"name\":\"vm-updated\"}"), put.response, + "/api/vms/vm-1", Negotiation.OutFormat.JSON, newServlet()); + verify(put.response).setStatus(200); + assertEquals("vm-updated", updateCaptor.getValue().getName()); + assertContains(put.body(), "vm-updated"); + } + + @Test + public void testHandleGetByIdDeleteAndPowerActions() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getInstance("vm-1", true, true, true, true)).thenReturn(withId(new Vm(), "vm-1")); + + final VmAction deleteAction = new VmAction(); + deleteAction.setStatus("deleted"); + final VmAction startAction = new VmAction(); + startAction.setStatus("starting"); + final VmAction stopAction = new VmAction(); + stopAction.setStatus("stopping"); + final VmAction shutdownAction = new VmAction(); + shutdownAction.setStatus("shutting_down"); + when(handler.serverAdapter.deleteInstance("vm-1", true)).thenReturn(deleteAction); + when(handler.serverAdapter.startInstance("vm-1", false)).thenReturn(startAction); + when(handler.serverAdapter.stopInstance("vm-1", false)).thenReturn(stopAction); + when(handler.serverAdapter.shutdownInstance("vm-1", false)).thenReturn(shutdownAction); + + final ResponseCapture get = newResponse(); + handler.handle(newRequest("GET", Map.of( + "all_content", "true", + "follow", "tags,disk_attachments.disk,nics.reporteddevices"), null, null), + get.response, "/api/vms/vm-1", Negotiation.OutFormat.JSON, newServlet()); + verify(get.response).setStatus(200); + assertContains(get.body(), "\"id\":\"vm-1\""); + + final ResponseCapture delete = newResponse(); + handler.handle(newRequest("DELETE", Map.of("async", "true"), null, null), delete.response, + "/api/vms/vm-1", Negotiation.OutFormat.JSON, newServlet()); + verify(handler.serverAdapter).deleteInstance("vm-1", true); + verify(delete.response).setStatus(200); + assertContains(delete.body(), "deleted"); + + final ResponseCapture start = newResponse(); + handler.handle(newRequest("POST"), start.response, "/api/vms/vm-1/start", Negotiation.OutFormat.JSON, newServlet()); + verify(start.response).setStatus(202); + assertContains(start.body(), "starting"); + + final ResponseCapture stop = newResponse(); + handler.handle(newRequest("POST"), stop.response, "/api/vms/vm-1/stop", Negotiation.OutFormat.JSON, newServlet()); + verify(stop.response).setStatus(202); + assertContains(stop.body(), "stopping"); + + final ResponseCapture shutdown = newResponse(); + handler.handle(newRequest("POST"), shutdown.response, "/api/vms/vm-1/shutdown", Negotiation.OutFormat.JSON, newServlet()); + verify(shutdown.response).setStatus(202); + assertContains(shutdown.body(), "shutting_down"); + } + + @Test + public void testHandleDiskAttachmentAndNicRoutes() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listDiskAttachmentsByInstanceUuid("vm-1")) + .thenReturn(List.of(withId(new DiskAttachment(), "attach-1"))); + when(handler.serverAdapter.listNicsByInstanceUuid("vm-1")) + .thenReturn(List.of(withId(new Nic(), "nic-1"))); + + final ArgumentCaptor diskAttachmentCaptor = ArgumentCaptor.forClass(DiskAttachment.class); + final DiskAttachment createdAttachment = withId(new DiskAttachment(), "attach-created"); + createdAttachment.setActive("true"); + when(handler.serverAdapter.attachInstanceDisk(eq("vm-1"), diskAttachmentCaptor.capture())).thenReturn(createdAttachment); + + final ArgumentCaptor nicCaptor = ArgumentCaptor.forClass(Nic.class); + final Nic createdNic = withId(new Nic(), "nic-created"); + createdNic.setName("nic-created"); + when(handler.serverAdapter.attachInstanceNic(eq("vm-1"), nicCaptor.capture())).thenReturn(createdNic); + + final ResponseCapture attachments = newResponse(); + handler.handle(newRequest("GET"), attachments.response, "/api/vms/vm-1/diskattachments", Negotiation.OutFormat.JSON, newServlet()); + verify(attachments.response).setStatus(200); + assertContains(attachments.body(), "\"disk_attachment\":["); + + final ResponseCapture postAttachment = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"active\":\"true\"}"), postAttachment.response, + "/api/vms/vm-1/diskattachments", Negotiation.OutFormat.JSON, newServlet()); + verify(postAttachment.response).setStatus(201); + assertEquals("true", diskAttachmentCaptor.getValue().getActive()); + assertContains(postAttachment.body(), "attach-created"); + + final ResponseCapture nics = newResponse(); + handler.handle(newRequest("GET"), nics.response, "/api/vms/vm-1/nics", Negotiation.OutFormat.JSON, newServlet()); + verify(nics.response).setStatus(200); + assertContains(nics.body(), "\"nic\":["); + + final ResponseCapture postNic = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"name\":\"nic-created\"}"), postNic.response, + "/api/vms/vm-1/nics", Negotiation.OutFormat.JSON, newServlet()); + verify(postNic.response).setStatus(201); + assertEquals("nic-created", nicCaptor.getValue().getName()); + assertContains(postNic.body(), "nic-created"); + } + + @Test + public void testHandleSnapshotRoutes() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listSnapshotsByInstanceUuid("vm-1")).thenReturn(List.of(withId(new Snapshot(), "snap-1"))); + when(handler.serverAdapter.getSnapshot("snap-1")).thenReturn(withId(new Snapshot(), "snap-1")); + + final ArgumentCaptor createCaptor = ArgumentCaptor.forClass(Snapshot.class); + final Snapshot createdSnapshot = withId(new Snapshot(), "snap-created"); + createdSnapshot.setDescription("created snapshot"); + when(handler.serverAdapter.createInstanceSnapshot(eq("vm-1"), createCaptor.capture())).thenReturn(createdSnapshot); + + final ResourceAction deleteAction = new ResourceAction(); + deleteAction.setStatus("deleted"); + final ResourceAction restoreAction = new ResourceAction(); + restoreAction.setStatus("restored"); + when(handler.serverAdapter.deleteSnapshot("snap-1", true)).thenReturn(deleteAction); + when(handler.serverAdapter.revertInstanceToSnapshot("snap-1", false)).thenReturn(restoreAction); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/vms/vm-1/snapshots", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"snapshot\":["); + + final ResponseCapture post = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"description\":\"created snapshot\"}"), post.response, + "/api/vms/vm-1/snapshots", Negotiation.OutFormat.JSON, newServlet()); + verify(post.response).setStatus(202); + assertEquals("created snapshot", createCaptor.getValue().getDescription()); + assertContains(post.body(), "snap-created"); + + final ResponseCapture get = newResponse(); + handler.handle(newRequest("GET"), get.response, "/api/vms/vm-1/snapshots/snap-1", Negotiation.OutFormat.JSON, newServlet()); + verify(get.response).setStatus(200); + assertContains(get.body(), "snap-1"); + + final ResponseCapture delete = newResponse(); + handler.handle(newRequest("DELETE", Map.of("async", "true"), null, null), delete.response, + "/api/vms/vm-1/snapshots/snap-1", Negotiation.OutFormat.JSON, newServlet()); + verify(delete.response).setStatus(202); + assertContains(delete.body(), "deleted"); + + final ResponseCapture restore = newResponse(); + handler.handle(newRequest("POST"), restore.response, "/api/vms/vm-1/snapshots/snap-1/restore", Negotiation.OutFormat.JSON, newServlet()); + verify(restore.response).setStatus(202); + assertContains(restore.body(), "restored"); + } + + @Test + public void testHandleBackupRoutes() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listBackupsByInstanceUuid("vm-1")).thenReturn(List.of(withId(new Backup(), "backup-1"))); + when(handler.serverAdapter.getBackup("backup-1")).thenReturn(withId(new Backup(), "backup-1")); + when(handler.serverAdapter.listDisksByBackupUuid("backup-1")).thenReturn(List.of(withId(new Disk(), "disk-1"))); + + final ArgumentCaptor createCaptor = ArgumentCaptor.forClass(Backup.class); + final Backup createdBackup = withId(new Backup(), "backup-created"); + createdBackup.setName("backup-created"); + when(handler.serverAdapter.createInstanceBackup(eq("vm-1"), createCaptor.capture())).thenReturn(createdBackup); + + final Backup finalizedBackup = withId(new Backup(), "backup-1"); + finalizedBackup.setPhase("finalized"); + when(handler.serverAdapter.finalizeBackup("vm-1", "backup-1")).thenReturn(finalizedBackup); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/vms/vm-1/backups", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"backup\":["); + + final ResponseCapture post = newResponse(); + handler.handle(newRequest("POST", Map.of(), "application/json", "{\"name\":\"backup-created\"}"), post.response, + "/api/vms/vm-1/backups", Negotiation.OutFormat.JSON, newServlet()); + verify(post.response).setStatus(200); + assertEquals("backup-created", createCaptor.getValue().getName()); + assertContains(post.body(), "backup-created"); + + final ResponseCapture get = newResponse(); + handler.handle(newRequest("GET"), get.response, "/api/vms/vm-1/backups/backup-1", Negotiation.OutFormat.JSON, newServlet()); + verify(get.response).setStatus(200); + assertContains(get.body(), "backup-1"); + + final ResponseCapture disks = newResponse(); + handler.handle(newRequest("GET"), disks.response, "/api/vms/vm-1/backups/backup-1/disks", Negotiation.OutFormat.JSON, newServlet()); + verify(disks.response).setStatus(200); + assertContains(disks.body(), "\"disk\":["); + + final ResponseCapture finalize = newResponse(); + handler.handle(newRequest("POST"), finalize.response, "/api/vms/vm-1/backups/backup-1/finalize", Negotiation.OutFormat.JSON, newServlet()); + verify(finalize.response).setStatus(200); + assertContains(finalize.body(), "finalized"); + } + + @Test + public void testHandleCheckpointRoutes() throws Exception { + final VmsRouteHandler handler = new VmsRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listCheckpointsByInstanceUuid("vm-1")).thenReturn(List.of(withId(new Checkpoint(), "chk-1"))); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/vms/vm-1/checkpoints", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"checkpoints\":["); + + final ResponseCapture delete = newResponse(); + handler.handle(newRequest("DELETE"), delete.response, "/api/vms/vm-1/checkpoints/chk-1", Negotiation.OutFormat.JSON, newServlet()); + verify(handler.serverAdapter).deleteCheckpoint("vm-1", "chk-1"); + verify(delete.response).setStatus(200); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandlerTest.java new file mode 100644 index 00000000000..c22d2c402ed --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/VnicProfilesRouteHandlerTest.java @@ -0,0 +1,63 @@ +// 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.veeam.api; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.veeam.api.dto.VnicProfile; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; + +public class VnicProfilesRouteHandlerTest extends RouteHandlerTestSupport { + + @Test + public void testHandleGetListAndById() throws Exception { + final VnicProfilesRouteHandler handler = new VnicProfilesRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.listAllVnicProfiles(null, null)).thenReturn(List.of(withId(new VnicProfile(), "vnic-1"))); + when(handler.serverAdapter.getVnicProfile("vnic-1")).thenReturn(withId(new VnicProfile(), "vnic-1")); + + final ResponseCapture list = newResponse(); + handler.handle(newRequest("GET"), list.response, "/api/vnicprofiles", Negotiation.OutFormat.JSON, newServlet()); + verify(list.response).setStatus(200); + assertContains(list.body(), "\"vnic_profile\":["); + + final ResponseCapture item = newResponse(); + handler.handle(newRequest("GET"), item.response, "/api/vnicprofiles/vnic-1", Negotiation.OutFormat.JSON, newServlet()); + verify(item.response).setStatus(200); + assertContains(item.body(), "\"id\":\"vnic-1\""); + } + + @Test + public void testHandleMissingVnicProfileReturnsNotFound() throws Exception { + final VnicProfilesRouteHandler handler = new VnicProfilesRouteHandler(); + handler.serverAdapter = mock(org.apache.cloudstack.veeam.adapter.ServerAdapter.class); + when(handler.serverAdapter.getVnicProfile("missing")).thenThrow(new InvalidParameterValueException("missing vnic")); + + final ResponseCapture response = newResponse(); + handler.handle(newRequest("GET"), response.response, "/api/vnicprofiles/missing", Negotiation.OutFormat.JSON, newServlet()); + + verify(response.response).setStatus(404); + assertContains(response.body(), "missing vnic"); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverterTest.java new file mode 100644 index 00000000000..503fd05104b --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverterTest.java @@ -0,0 +1,85 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.jobs.JobInfo; +import org.apache.cloudstack.veeam.api.dto.Job; +import org.apache.cloudstack.veeam.api.dto.ResourceAction; +import org.junit.Test; + +import com.cloud.api.query.vo.AsyncJobJoinVO; + +public class AsyncJobJoinVOToJobConverterTest { + + @Test + public void testToJob_MapsSucceededStatusAndOwnerRef() { + final AsyncJobJoinVO vo = mock(AsyncJobJoinVO.class); + when(vo.getUuid()).thenReturn("job-1"); + when(vo.getUserUuid()).thenReturn("user-1"); + when(vo.getCreated()).thenReturn(new Date(1000L)); + when(vo.getStatus()).thenReturn(JobInfo.Status.SUCCEEDED.ordinal()); + + final Job job = AsyncJobJoinVOToJobConverter.toJob(vo); + + assertEquals("job-1", job.getId()); + assertEquals("finished", job.getStatus()); + assertEquals(Long.valueOf(1000L), job.getStartTime()); + assertNotNull(job.getEndTime()); + assertEquals("user-1", job.getOwner().getId()); + } + + @Test + public void testToJob_MapsInProgressToStartedAndNoEndTime() { + final AsyncJobJoinVO vo = mock(AsyncJobJoinVO.class); + when(vo.getUuid()).thenReturn("job-2"); + when(vo.getUserUuid()).thenReturn("user-2"); + when(vo.getCreated()).thenReturn(new Date(2000L)); + when(vo.getStatus()).thenReturn(JobInfo.Status.IN_PROGRESS.ordinal()); + + final Job job = AsyncJobJoinVOToJobConverter.toJob(vo); + + assertEquals("started", job.getStatus()); + assertNull(job.getEndTime()); + } + + @Test + public void testToActionAndToJobList() { + final AsyncJobJoinVO vo = mock(AsyncJobJoinVO.class); + when(vo.getUuid()).thenReturn("job-3"); + when(vo.getUserUuid()).thenReturn("user-3"); + when(vo.getCreated()).thenReturn(new Date(3000L)); + when(vo.getStatus()).thenReturn(JobInfo.Status.CANCELLED.ordinal()); + + final ResourceAction action = AsyncJobJoinVOToJobConverter.toAction(vo); + assertEquals("complete", action.getStatus()); + assertEquals("job-3", action.getJob().getId()); + + final List jobs = AsyncJobJoinVOToJobConverter.toJobList(List.of(vo)); + assertEquals(1, jobs.size()); + assertEquals("aborted", jobs.get(0).getStatus()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/BackupVOToBackupConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/BackupVOToBackupConverterTest.java new file mode 100644 index 00000000000..af6860c7d77 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/BackupVOToBackupConverterTest.java @@ -0,0 +1,99 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.backup.BackupVO; +import org.apache.cloudstack.veeam.api.dto.Disk; +import org.junit.Test; + +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.vm.UserVmVO; + +public class BackupVOToBackupConverterTest { + + @Test + public void testToBackup_MapsCoreFieldsAndResolvedRefs() { + final BackupVO backupVO = mock(BackupVO.class); + when(backupVO.getUuid()).thenReturn("bkp-1"); + when(backupVO.getName()).thenReturn("backup-1"); + when(backupVO.getDescription()).thenReturn("desc-1"); + when(backupVO.getDate()).thenReturn(new Date(1000L)); + when(backupVO.getStatus()).thenReturn(Backup.Status.ReadyForTransfer); + when(backupVO.getFromCheckpointId()).thenReturn("cp-1"); + when(backupVO.getToCheckpointId()).thenReturn("cp-2"); + when(backupVO.getVmId()).thenReturn(101L); + when(backupVO.getHostId()).thenReturn(201L); + + final UserVmVO vmVO = mock(UserVmVO.class); + when(vmVO.getUuid()).thenReturn("vm-1"); + final HostJoinVO hostVO = mock(HostJoinVO.class); + when(hostVO.getUuid()).thenReturn("host-1"); + + final org.apache.cloudstack.veeam.api.dto.Backup backup = BackupVOToBackupConverter.toBackup( + backupVO, + id -> vmVO, + id -> hostVO, + vo -> List.of(new Disk()) + ); + + assertEquals("bkp-1", backup.getId()); + assertEquals("backup-1", backup.getName()); + assertEquals("desc-1", backup.getDescription()); + assertEquals(Long.valueOf(1000L), backup.getCreationDate()); + assertEquals("ready", backup.getPhase()); + assertEquals("cp-1", backup.getFromCheckpointId()); + assertEquals("cp-2", backup.getToCheckpointId()); + assertEquals("vm-1", backup.getVm().getId()); + assertEquals("host-1", backup.getHost().getId()); + assertNotNull(backup.getDisks()); + } + + @Test + public void testToBackup_PhaseMappingForDifferentStatuses() { + final BackupVO queued = mock(BackupVO.class); + when(queued.getUuid()).thenReturn("b1"); + when(queued.getDate()).thenReturn(new Date(1L)); + when(queued.getStatus()).thenReturn(Backup.Status.Queued); + when(queued.getVmId()).thenReturn(1L); + + final BackupVO finalizing = mock(BackupVO.class); + when(finalizing.getUuid()).thenReturn("b2"); + when(finalizing.getDate()).thenReturn(new Date(2L)); + when(finalizing.getStatus()).thenReturn(Backup.Status.FinalizingTransfer); + when(finalizing.getVmId()).thenReturn(2L); + + final BackupVO failed = mock(BackupVO.class); + when(failed.getUuid()).thenReturn("b3"); + when(failed.getDate()).thenReturn(new Date(3L)); + when(failed.getStatus()).thenReturn(Backup.Status.Failed); + when(failed.getVmId()).thenReturn(3L); + + assertEquals("initializing", BackupVOToBackupConverter.toBackup(queued, null, null, null).getPhase()); + assertEquals("finalizing", BackupVOToBackupConverter.toBackup(finalizing, null, null, null).getPhase()); + assertEquals("failed", BackupVOToBackupConverter.toBackup(failed, null, null, null).getPhase()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ClusterVOToClusterConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ClusterVOToClusterConverterTest.java new file mode 100644 index 00000000000..1ac57f44949 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ClusterVOToClusterConverterTest.java @@ -0,0 +1,81 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.Cluster; +import org.junit.Test; + +import com.cloud.api.query.vo.DataCenterJoinVO; +import com.cloud.cpu.CPU; +import com.cloud.dc.ClusterVO; + +public class ClusterVOToClusterConverterTest { + + @Test + public void testToCluster_MapsDefaultsAndResolvedDataCenter() { + final ClusterVO vo = mock(ClusterVO.class); + when(vo.getUuid()).thenReturn("cluster-1"); + when(vo.getName()).thenReturn("cluster-a"); + when(vo.getArch()).thenReturn(CPU.CPUArch.amd64); + when(vo.getDataCenterId()).thenReturn(11L); + + final DataCenterJoinVO zone = mock(DataCenterJoinVO.class); + when(zone.getUuid()).thenReturn("dc-1"); + + final Cluster cluster = ClusterVOToClusterConverter.toCluster(vo, id -> zone); + + assertEquals("cluster-1", cluster.getId()); + assertEquals("cluster-a", cluster.getName()); + assertEquals("x86_64", cluster.getCpu().getArchitecture()); + assertEquals("dc-1", cluster.getDataCenter().getId()); + assertEquals("urandom", cluster.getRequiredRngSources().requiredRngSource.get(0)); + assertEquals("networks", cluster.getLink().get(0).getRel()); + assertNotNull(cluster.getSchedulingPolicy()); + assertNotNull(cluster.getMacPool()); + assertTrue(cluster.getHref().contains("/api/clusters/cluster-1")); + } + + @Test + public void testToClusterList_ConvertsAllItems() { + final ClusterVO first = mock(ClusterVO.class); + when(first.getUuid()).thenReturn("c1"); + when(first.getName()).thenReturn("c1"); + when(first.getArch()).thenReturn(CPU.CPUArch.x86); + when(first.getDataCenterId()).thenReturn(1L); + + final ClusterVO second = mock(ClusterVO.class); + when(second.getUuid()).thenReturn("c2"); + when(second.getName()).thenReturn("c2"); + when(second.getArch()).thenReturn(CPU.CPUArch.amd64); + when(second.getDataCenterId()).thenReturn(2L); + + final List clusters = ClusterVOToClusterConverter.toClusterList(List.of(first, second), id -> null); + + assertEquals(2, clusters.size()); + assertEquals("c1", clusters.get(0).getId()); + assertEquals("c2", clusters.get(1).getId()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/DataCenterJoinVOToDataCenterConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/DataCenterJoinVOToDataCenterConverterTest.java new file mode 100644 index 00000000000..59d14a5545c --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/DataCenterJoinVOToDataCenterConverterTest.java @@ -0,0 +1,88 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.DataCenter; +import org.junit.Test; + +import com.cloud.api.query.vo.DataCenterJoinVO; +import com.cloud.org.Grouping; + +public class DataCenterJoinVOToDataCenterConverterTest { + + @Test + public void testToDataCenter_MapsIdentityStatusAndLinks() { + final DataCenterJoinVO zone = mock(DataCenterJoinVO.class); + when(zone.getUuid()).thenReturn("dc-1"); + when(zone.getName()).thenReturn("zone-a"); + when(zone.getDescription()).thenReturn("desc-a"); + when(zone.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + + final DataCenter dc = DataCenterJoinVOToDataCenterConverter.toDataCenter(zone); + + assertEquals("dc-1", dc.getId()); + assertEquals("zone-a", dc.getName()); + assertEquals("desc-a", dc.getDescription()); + assertEquals("up", dc.getStatus()); + assertNotNull(dc.getVersion()); + assertNotNull(dc.getSupportedVersions()); + assertEquals(3, dc.getLink().size()); + assertEquals("cluster", dc.getLink().get(0).getRel()); + } + + @Test + public void testToDataCenter_DisabledZoneMapsToDown() { + final DataCenterJoinVO zone = mock(DataCenterJoinVO.class); + when(zone.getUuid()).thenReturn("dc-2"); + when(zone.getName()).thenReturn("zone-b"); + when(zone.getDescription()).thenReturn("desc-b"); + when(zone.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled); + + final DataCenter dc = DataCenterJoinVOToDataCenterConverter.toDataCenter(zone); + + assertEquals("down", dc.getStatus()); + } + + @Test + public void testToDcList_ConvertsAllItems() { + final DataCenterJoinVO first = mock(DataCenterJoinVO.class); + when(first.getUuid()).thenReturn("dc-1"); + when(first.getName()).thenReturn("zone-a"); + when(first.getDescription()).thenReturn("desc-a"); + when(first.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled); + + final DataCenterJoinVO second = mock(DataCenterJoinVO.class); + when(second.getUuid()).thenReturn("dc-2"); + when(second.getName()).thenReturn("zone-b"); + when(second.getDescription()).thenReturn("desc-b"); + when(second.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled); + + final List result = DataCenterJoinVOToDataCenterConverter.toDCList(List.of(first, second)); + + assertEquals(2, result.size()); + assertEquals("dc-1", result.get(0).getId()); + assertEquals("dc-2", result.get(1).getId()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/HostJoinVOToHostConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/HostJoinVOToHostConverterTest.java new file mode 100644 index 00000000000..e5ab87b40c5 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/HostJoinVOToHostConverterTest.java @@ -0,0 +1,123 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.Host; +import org.junit.Test; + +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.host.Status; +import com.cloud.resource.ResourceState; + +public class HostJoinVOToHostConverterTest { + + @Test + public void testToHost_MapsUpHostAndCoreFields() { + final HostJoinVO vo = mock(HostJoinVO.class); + when(vo.getUuid()).thenReturn("host-1"); + when(vo.getName()).thenReturn("kvm-1"); + when(vo.getPrivateIpAddress()).thenReturn("10.10.10.11"); + when(vo.isInMaintenanceStates()).thenReturn(false); + when(vo.getStatus()).thenReturn(Status.Up); + when(vo.getResourceState()).thenReturn(ResourceState.Enabled); + when(vo.getClusterUuid()).thenReturn("cl-1"); + when(vo.getSpeed()).thenReturn(2400L); + when(vo.getCpuSockets()).thenReturn(2); + when(vo.getCpus()).thenReturn(8); + when(vo.getTotalMemory()).thenReturn(16000L); + when(vo.getMemUsedCapacity()).thenReturn(4000L); + + final Host host = HostJoinVOToHostConverter.toHost(vo); + + assertEquals("host-1", host.getId()); + assertEquals("kvm-1", host.getName()); + assertEquals("10.10.10.11", host.getAddress()); + assertEquals("up", host.getStatus()); + assertEquals("cl-1", host.getCluster().getId()); + assertEquals("2400", host.getCpu().getSpeed()); + assertEquals("16000", host.getMemory()); + assertEquals("12000", host.getMaxSchedulingMemory()); + assertTrue(host.getHref().contains("/api/hosts/host-1")); + } + + @Test + public void testToHost_MapsMaintenanceAndFallbackName() { + final HostJoinVO vo = mock(HostJoinVO.class); + when(vo.getUuid()).thenReturn("host-2"); + when(vo.getName()).thenReturn(null); + when(vo.getPrivateIpAddress()).thenReturn("10.10.10.12"); + when(vo.isInMaintenanceStates()).thenReturn(true); + when(vo.getStatus()).thenReturn(Status.Down); + when(vo.getResourceState()).thenReturn(ResourceState.Disabled); + when(vo.getClusterUuid()).thenReturn("cl-2"); + when(vo.getSpeed()).thenReturn(2200L); + when(vo.getCpuSockets()).thenReturn(1); + when(vo.getCpus()).thenReturn(4); + when(vo.getTotalMemory()).thenReturn(8000L); + when(vo.getMemUsedCapacity()).thenReturn(2000L); + + final Host host = HostJoinVOToHostConverter.toHost(vo); + + assertEquals("host-host-2", host.getName()); + assertEquals("maintenance", host.getStatus()); + } + + @Test + public void testToHostList_ConvertsAllEntries() { + final HostJoinVO first = mock(HostJoinVO.class); + when(first.getUuid()).thenReturn("h1"); + when(first.getName()).thenReturn("h1"); + when(first.getPrivateIpAddress()).thenReturn("1.1.1.1"); + when(first.isInMaintenanceStates()).thenReturn(false); + when(first.getStatus()).thenReturn(Status.Up); + when(first.getResourceState()).thenReturn(ResourceState.Enabled); + when(first.getClusterUuid()).thenReturn("c1"); + when(first.getSpeed()).thenReturn(1000L); + when(first.getCpuSockets()).thenReturn(1); + when(first.getCpus()).thenReturn(1); + when(first.getTotalMemory()).thenReturn(1024L); + when(first.getMemUsedCapacity()).thenReturn(24L); + + final HostJoinVO second = mock(HostJoinVO.class); + when(second.getUuid()).thenReturn("h2"); + when(second.getName()).thenReturn("h2"); + when(second.getPrivateIpAddress()).thenReturn("2.2.2.2"); + when(second.isInMaintenanceStates()).thenReturn(false); + when(second.getStatus()).thenReturn(Status.Down); + when(second.getResourceState()).thenReturn(ResourceState.Disabled); + when(second.getClusterUuid()).thenReturn("c2"); + when(second.getSpeed()).thenReturn(2000L); + when(second.getCpuSockets()).thenReturn(1); + when(second.getCpus()).thenReturn(2); + when(second.getTotalMemory()).thenReturn(2048L); + when(second.getMemUsedCapacity()).thenReturn(48L); + + final List hosts = HostJoinVOToHostConverter.toHostList(List.of(first, second)); + + assertEquals(2, hosts.size()); + assertEquals("h1", hosts.get(0).getId()); + assertEquals("h2", hosts.get(1).getId()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverterTest.java new file mode 100644 index 00000000000..a5d88b0f0a6 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ImageTransferVOToImageTransferConverterTest.java @@ -0,0 +1,41 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class ImageTransferVOToImageTransferConverterTest { + + @Test + public void testConverterExposesExpectedApiMethods() { + final java.lang.reflect.Method[] methods = ImageTransferVOToImageTransferConverter.class.getDeclaredMethods(); + boolean hasSingle = false; + boolean hasList = false; + for (java.lang.reflect.Method method : methods) { + if ("toImageTransfer".equals(method.getName())) { + hasSingle = true; + } + if ("toImageTransferList".equals(method.getName())) { + hasList = true; + } + } + assertTrue(hasSingle); + assertTrue(hasList); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NetworkVOToNetworkConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NetworkVOToNetworkConverterTest.java new file mode 100644 index 00000000000..f3323e18fa6 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NetworkVOToNetworkConverterTest.java @@ -0,0 +1,113 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.Network; +import org.junit.Test; + +import com.cloud.api.query.vo.DataCenterJoinVO; +import com.cloud.network.Networks; +import com.cloud.network.dao.NetworkVO; + +public class NetworkVOToNetworkConverterTest { + + @Test + public void testToNetwork_MapsFieldsAndDataCenterRef() { + final NetworkVO vo = mock(NetworkVO.class); + when(vo.getUuid()).thenReturn("net-1"); + when(vo.getName()).thenReturn("guest-net"); + when(vo.getDisplayText()).thenReturn("Guest network"); + when(vo.getPrivateMtu()).thenReturn(1450); + when(vo.getDataCenterId()).thenReturn(10L); + + final DataCenterJoinVO dc = mock(DataCenterJoinVO.class); + when(dc.getUuid()).thenReturn("dc-1"); + + final Network dto = NetworkVOToNetworkConverter.toNetwork(vo, id -> dc); + + assertEquals("net-1", dto.getId()); + assertEquals("guest-net", dto.getName()); + assertEquals("Guest network", dto.getDescription()); + assertEquals("", dto.getComment()); + assertEquals("1450", dto.getMtu()); + assertEquals("false", dto.getPortIsolation()); + assertEquals("false", dto.getStp()); + assertEquals("guest-net", dto.getVdsmName()); + assertNotNull(dto.getUsages()); + assertEquals(1, dto.getUsages().getItems().size()); + assertEquals("vm", dto.getUsages().getItems().get(0)); + assertNotNull(dto.getLink()); + assertTrue(dto.getLink().isEmpty()); + assertEquals("dc-1", dto.getDataCenter().getId()); + assertTrue(dto.getHref().contains("/api/networks/net-1")); + } + + @Test + public void testToNetwork_UsesFallbackNameAndSkipsBlankDataCenterUuid() { + final NetworkVO vo = mock(NetworkVO.class); + when(vo.getUuid()).thenReturn("net-2"); + when(vo.getName()).thenReturn(null); + when(vo.getTrafficType()).thenReturn(Networks.TrafficType.Guest); + when(vo.getDisplayText()).thenReturn("Fallback network"); + when(vo.getPrivateMtu()).thenReturn(null); + when(vo.getDataCenterId()).thenReturn(20L); + + final DataCenterJoinVO dc = mock(DataCenterJoinVO.class); + when(dc.getUuid()).thenReturn(""); + + final Network dto = NetworkVOToNetworkConverter.toNetwork(vo, id -> dc); + + assertEquals("Guest-net-2", dto.getName()); + assertEquals("0", dto.getMtu()); + assertEquals("Guest-net-2", dto.getVdsmName()); + assertNull(dto.getDataCenter()); + } + + @Test + public void testToNetworkList_ConvertsAllItemsInOrder() { + final NetworkVO first = mock(NetworkVO.class); + when(first.getUuid()).thenReturn("net-1"); + when(first.getName()).thenReturn("first-net"); + when(first.getDisplayText()).thenReturn("First network"); + when(first.getPrivateMtu()).thenReturn(1400); + + final NetworkVO second = mock(NetworkVO.class); + when(second.getUuid()).thenReturn("net-2"); + when(second.getName()).thenReturn(null); + when(second.getTrafficType()).thenReturn(Networks.TrafficType.Control); + when(second.getDisplayText()).thenReturn("Second network"); + when(second.getPrivateMtu()).thenReturn(null); + + final List result = NetworkVOToNetworkConverter.toNetworkList(List.of(first, second), null); + + assertEquals(2, result.size()); + assertEquals("net-1", result.get(0).getId()); + assertEquals("first-net", result.get(0).getName()); + assertEquals("net-2", result.get(1).getId()); + assertEquals("Control-net-2", result.get(1).getName()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NetworkVOToVnicProfileConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NetworkVOToVnicProfileConverterTest.java new file mode 100644 index 00000000000..c8a57e90383 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NetworkVOToVnicProfileConverterTest.java @@ -0,0 +1,100 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.VnicProfile; +import org.junit.Test; + +import com.cloud.api.query.vo.DataCenterJoinVO; +import com.cloud.network.Networks; +import com.cloud.network.dao.NetworkVO; + +public class NetworkVOToVnicProfileConverterTest { + + @Test + public void testToVnicProfile_MapsNetworkAndDataCenterRefs() { + final NetworkVO vo = mock(NetworkVO.class); + when(vo.getUuid()).thenReturn("net-3"); + when(vo.getName()).thenReturn("profile-net"); + when(vo.getDisplayText()).thenReturn("Profile network"); + when(vo.getDataCenterId()).thenReturn(30L); + + final DataCenterJoinVO dc = mock(DataCenterJoinVO.class); + when(dc.getUuid()).thenReturn("dc-3"); + + final VnicProfile profile = NetworkVOToVnicProfileConverter.toVnicProfile(vo, id -> dc); + + assertEquals("net-3", profile.getId()); + assertEquals("profile-net", profile.getName()); + assertEquals("Profile network", profile.getDescription()); + assertEquals("net-3", profile.getNetwork().getId()); + assertEquals("dc-3", profile.getDataCenter().getId()); + assertTrue(profile.getHref().contains("/api/vnicprofiles/net-3")); + assertTrue(profile.getNetwork().getHref().contains("/api/networks/net-3")); + } + + @Test + public void testToVnicProfile_UsesFallbackNameAndOmitsBlankDataCenterUuid() { + final NetworkVO vo = mock(NetworkVO.class); + when(vo.getUuid()).thenReturn("net-4"); + when(vo.getName()).thenReturn(null); + when(vo.getTrafficType()).thenReturn(Networks.TrafficType.Management); + when(vo.getDisplayText()).thenReturn("Mgmt network"); + when(vo.getDataCenterId()).thenReturn(40L); + + final DataCenterJoinVO dc = mock(DataCenterJoinVO.class); + when(dc.getUuid()).thenReturn(""); + + final VnicProfile profile = NetworkVOToVnicProfileConverter.toVnicProfile(vo, id -> dc); + + assertEquals("Management-net-4", profile.getName()); + assertEquals("Mgmt network", profile.getDescription()); + assertEquals("net-4", profile.getNetwork().getId()); + assertNull(profile.getDataCenter()); + } + + @Test + public void testToVnicProfileList_ConvertsAllItemsInOrder() { + final NetworkVO first = mock(NetworkVO.class); + when(first.getUuid()).thenReturn("net-1"); + when(first.getName()).thenReturn("profile-a"); + when(first.getDisplayText()).thenReturn("Profile A"); + + final NetworkVO second = mock(NetworkVO.class); + when(second.getUuid()).thenReturn("net-2"); + when(second.getName()).thenReturn(null); + when(second.getTrafficType()).thenReturn(Networks.TrafficType.Control); + when(second.getDisplayText()).thenReturn("Profile B"); + + final List result = NetworkVOToVnicProfileConverter.toVnicProfileList(List.of(first, second), null); + + assertEquals(2, result.size()); + assertEquals("net-1", result.get(0).getId()); + assertEquals("profile-a", result.get(0).getName()); + assertEquals("net-2", result.get(1).getId()); + assertEquals("Control-net-2", result.get(1).getName()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverterTest.java new file mode 100644 index 00000000000..cb60d8e081e --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/NicVOToNicConverterTest.java @@ -0,0 +1,66 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.veeam.api.dto.Nic; +import org.junit.Test; + +import com.cloud.network.dao.NetworkVO; + +public class NicVOToNicConverterTest { + + @Test + public void testToNic_MapsVmHrefIpAndVnicProfile() { + final com.cloud.vm.NicVO vo = mock(com.cloud.vm.NicVO.class); + when(vo.getUuid()).thenReturn("nic-1"); + when(vo.getReserver()).thenReturn("eth0"); + when(vo.getMacAddress()).thenReturn("02:00:00:00:00:01"); + when(vo.getIPv4Address()).thenReturn("10.1.1.10"); + when(vo.getIPv4Gateway()).thenReturn("10.1.1.1"); + when(vo.getNetworkId()).thenReturn(10L); + + final NetworkVO network = mock(NetworkVO.class); + when(network.getUuid()).thenReturn("net-1"); + + final Nic nic = NicVOToNicConverter.toNic(vo, "vm-1", id -> network); + + assertEquals("nic-1", nic.getId()); + assertEquals("virtio", nic.getInterfaceType()); + assertEquals("vm-1", nic.getVm().getId()); + assertTrue(nic.getHref().contains("/api/vms/vm-1/nics/nic-1")); + assertEquals("net-1", nic.getVnicProfile().getId()); + assertNotNull(nic.getReportedDevices()); + assertEquals("v4", nic.getReportedDevices().getItems().get(0).getIps().getItems().get(0).getVersion()); + } + + @Test(expected = NullPointerException.class) + public void testToNic_BlankVmUuidCurrentBehaviorThrowsNpe() { + final com.cloud.vm.NicVO vo = mock(com.cloud.vm.NicVO.class); + when(vo.getUuid()).thenReturn("nic-2"); + when(vo.getReserver()).thenReturn("eth1"); + when(vo.getMacAddress()).thenReturn("02:00:00:00:00:02"); + + NicVOToNicConverter.toNic(vo, "", null); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ResourceTagVOToTagConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ResourceTagVOToTagConverterTest.java new file mode 100644 index 00000000000..6c607ea1f8c --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/ResourceTagVOToTagConverterTest.java @@ -0,0 +1,87 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.BaseDto; +import org.apache.cloudstack.veeam.api.dto.Tag; +import org.junit.Test; + +import com.cloud.server.ResourceTag; +import com.cloud.tags.ResourceTagVO; + +public class ResourceTagVOToTagConverterTest { + + @Test + public void testGetRootTagAndRootRef() { + final Tag root = ResourceTagVOToTagConverter.getRootTag(); + + assertEquals(BaseDto.ZERO_UUID, root.getId()); + assertEquals("root", root.getName()); + assertNotNull(ResourceTagVOToTagConverter.getRootTagRef().getHref()); + } + + @Test + public void testToTag_FromResourceTagVoWithVmReference() { + final ResourceTagVO vo = mock(ResourceTagVO.class); + when(vo.getKey()).thenReturn("env"); + when(vo.getValue()).thenReturn("prod"); + when(vo.getResourceType()).thenReturn(ResourceTag.ResourceObjectType.UserVm); + when(vo.getResourceUuid()).thenReturn("vm-1"); + + final Tag tag = ResourceTagVOToTagConverter.toTag(vo); + + assertEquals("prod", tag.getId()); + assertEquals("prod", tag.getName()); + assertNotNull(tag.getParent()); + assertNotNull(tag.getVm()); + assertEquals("vm-1", tag.getVm().getId()); + } + + @Test + public void testToTag_FromResourceTagVoWithoutVmReference() { + final ResourceTagVO vo = mock(ResourceTagVO.class); + when(vo.getKey()).thenReturn("scope"); + when(vo.getValue()).thenReturn("global"); + when(vo.getResourceType()).thenReturn(ResourceTag.ResourceObjectType.Volume); + + final Tag tag = ResourceTagVOToTagConverter.toTag(vo); + + assertEquals("global", tag.getId()); + assertNull(tag.getVm()); + } + + @Test + public void testToTagsAndToTagsFromValues() { + final ResourceTagVO vo = mock(ResourceTagVO.class); + when(vo.getKey()).thenReturn("team"); + when(vo.getValue()).thenReturn("ops"); + when(vo.getResourceType()).thenReturn(ResourceTag.ResourceObjectType.UserVm); + when(vo.getResourceUuid()).thenReturn("vm-2"); + + assertEquals(1, ResourceTagVOToTagConverter.toTags(List.of(vo)).size()); + assertEquals("ops", ResourceTagVOToTagConverter.toTagsFromValues(List.of("ops")).get(0).getId()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/StoreVOToStorageDomainConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/StoreVOToStorageDomainConverterTest.java new file mode 100644 index 00000000000..479dab2157c --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/StoreVOToStorageDomainConverterTest.java @@ -0,0 +1,99 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.StorageDomain; +import org.junit.Test; + +import com.cloud.api.query.vo.ImageStoreJoinVO; +import com.cloud.api.query.vo.StoragePoolJoinVO; +import com.cloud.storage.Storage; +import com.cloud.storage.StoragePoolStatus; + +public class StoreVOToStorageDomainConverterTest { + + @Test + public void testToStorageDomain_FromPrimaryPool() { + final StoragePoolJoinVO pool = mock(StoragePoolJoinVO.class); + when(pool.getUuid()).thenReturn("pool-1"); + when(pool.getName()).thenReturn("Primary-1"); + when(pool.getCapacityBytes()).thenReturn(1000L); + when(pool.getUsedBytes()).thenReturn(250L); + when(pool.getStatus()).thenReturn(StoragePoolStatus.Up); + when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + when(pool.getZoneUuid()).thenReturn("dc-1"); + + final StorageDomain sd = StoreVOToStorageDomainConverter.toStorageDomain(pool); + + assertEquals("pool-1", sd.getId()); + assertEquals("data", sd.getType()); + assertEquals("active", sd.getStatus()); + assertEquals("750", sd.getAvailable()); + assertEquals("250", sd.getUsed()); + assertEquals("1000", sd.getCommitted()); + assertEquals("nfs", sd.getStorage().getType()); + assertEquals("dc-1", sd.getDataCenters().getItems().get(0).getId()); + assertTrue(sd.getLink().stream().anyMatch(l -> "disks".equals(l.getRel()))); + } + + @Test + public void testToStorageDomain_FromImageStore() { + final ImageStoreJoinVO store = mock(ImageStoreJoinVO.class); + when(store.getUuid()).thenReturn("img-1"); + when(store.getName()).thenReturn("Secondary-1"); + when(store.getProviderName()).thenReturn("glance"); + when(store.getZoneUuid()).thenReturn("dc-2"); + + final StorageDomain sd = StoreVOToStorageDomainConverter.toStorageDomain(store); + + assertEquals("img-1", sd.getId()); + assertEquals("image", sd.getType()); + assertEquals("unattached", sd.getStatus()); + assertEquals("glance", sd.getStorage().getType()); + assertEquals("dc-2", sd.getDataCenters().getItems().get(0).getId()); + assertTrue(sd.getLink().stream().anyMatch(l -> "images".equals(l.getRel()))); + } + + @Test + public void testListConverters() { + final StoragePoolJoinVO pool = mock(StoragePoolJoinVO.class); + when(pool.getUuid()).thenReturn("p"); + when(pool.getName()).thenReturn("P"); + when(pool.getCapacityBytes()).thenReturn(10L); + when(pool.getUsedBytes()).thenReturn(2L); + when(pool.getStatus()).thenReturn(StoragePoolStatus.Up); + when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.Filesystem); + when(pool.getZoneUuid()).thenReturn("z"); + + final ImageStoreJoinVO store = mock(ImageStoreJoinVO.class); + when(store.getUuid()).thenReturn("s"); + when(store.getName()).thenReturn("S"); + when(store.getProviderName()).thenReturn("glance"); + when(store.getZoneUuid()).thenReturn("z"); + + assertEquals(1, StoreVOToStorageDomainConverter.toStorageDomainListFromPools(List.of(pool)).size()); + assertEquals(1, StoreVOToStorageDomainConverter.toStorageDomainListFromStores(List.of(store)).size()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverterTest.java new file mode 100644 index 00000000000..eb7442750ea --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/UserVmJoinVOToVmConverterTest.java @@ -0,0 +1,138 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.veeam.api.dto.DiskAttachment; +import org.apache.cloudstack.veeam.api.dto.Nic; +import org.apache.cloudstack.veeam.api.dto.Tag; +import org.apache.cloudstack.veeam.api.dto.Vm; +import org.junit.Test; + +import org.apache.cloudstack.api.ApiConstants; +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.api.query.vo.UserVmJoinVO; +import com.cloud.vm.VirtualMachine; + +public class UserVmJoinVOToVmConverterTest { + + @Test + public void testToVm_MapsUpStateWithBasicFields() { + final UserVmJoinVO src = mock(UserVmJoinVO.class); + when(src.getId()).thenReturn(101L); + when(src.getUuid()).thenReturn("vm-1"); + when(src.getName()).thenReturn("vm-1-name"); + when(src.getDisplayName()).thenReturn("vm-1-display"); + when(src.getInstanceName()).thenReturn("i-101"); + when(src.getState()).thenReturn(VirtualMachine.State.Running); + when(src.getCreated()).thenReturn(new Date(1000L)); + when(src.getLastUpdated()).thenReturn(new Date(2000L)); + when(src.getTemplateUuid()).thenReturn("tmpl-1"); + when(src.getHostUuid()).thenReturn("host-1"); + when(src.getRamSize()).thenReturn(512); + when(src.getArch()).thenReturn("x86_64"); + when(src.getCpu()).thenReturn(2); + when(src.getGuestOsDisplayName()).thenReturn("Linux"); + when(src.getServiceOfferingUuid()).thenReturn("offering-1"); + when(src.getAccountUuid()).thenReturn("acct-1"); + when(src.getAffinityGroupUuid()).thenReturn("ag-1"); + when(src.getUserDataUuid()).thenReturn("ud-1"); + + final Vm vm = UserVmJoinVOToVmConverter.toVm(src, null, null, null, null, null, false); + + assertEquals("vm-1", vm.getId()); + assertEquals("vm-1-name", vm.getName()); + assertEquals("vm-1-display", vm.getDescription()); + assertEquals("up", vm.getStatus()); + assertEquals(Long.valueOf(1000L), vm.getCreationTime()); + assertEquals(Long.valueOf(2000L), vm.getStartTime()); + assertNull(vm.getStopTime()); + assertEquals("536870912", vm.getMemory()); + assertEquals("x86_64", vm.getCpu().getArchitecture()); + assertEquals("host-1", vm.getHost().getId()); + assertNotNull(vm.getActions()); + assertEquals(3, vm.getActions().getItems().size()); + } + + @Test + public void testToVm_UsesResolversAndMapsDownState() { + final UserVmJoinVO src = mock(UserVmJoinVO.class); + when(src.getId()).thenReturn(202L); + when(src.getUuid()).thenReturn("vm-2"); + when(src.getName()).thenReturn("vm-2-name"); + when(src.getDisplayName()).thenReturn("vm-2-display"); + when(src.getInstanceName()).thenReturn("i-202"); + when(src.getState()).thenReturn(VirtualMachine.State.Stopped); + when(src.getCreated()).thenReturn(new Date(3000L)); + when(src.getLastUpdated()).thenReturn(new Date(4000L)); + when(src.getTemplateUuid()).thenReturn("tmpl-2"); + when(src.getHostUuid()).thenReturn(null); + when(src.getHostId()).thenReturn(22L); + when(src.getLastHostId()).thenReturn(null); + when(src.getRamSize()).thenReturn(1024); + when(src.getArch()).thenReturn("x86_64"); + when(src.getCpu()).thenReturn(4); + when(src.getGuestOsDisplayName()).thenReturn("Linux"); + when(src.getServiceOfferingUuid()).thenReturn("offering-2"); + when(src.getAccountUuid()).thenReturn("acct-2"); + when(src.getAffinityGroupUuid()).thenReturn("ag-2"); + when(src.getUserDataUuid()).thenReturn("ud-2"); + + final HostJoinVO hostVo = mock(HostJoinVO.class); + when(hostVo.getUuid()).thenReturn("host-2"); + when(hostVo.getClusterUuid()).thenReturn("cluster-2"); + + final Tag tag = new Tag(); + tag.setId("tag-1"); + + final DiskAttachment disk = new DiskAttachment(); + disk.setId("da-1"); + + final Nic nic = new Nic(); + nic.setId("nic-1"); + + final Vm vm = UserVmJoinVOToVmConverter.toVm( + src, + id -> hostVo, + id -> Map.of(ApiConstants.BootType.UEFI.toString(), "true"), + id -> List.of(tag), + id -> List.of(disk), + ignored -> List.of(nic), + false); + + assertEquals("down", vm.getStatus()); + assertEquals(Long.valueOf(4000L), vm.getStopTime()); + assertEquals("host-2", vm.getHost().getId()); + assertEquals("cluster-2", vm.getCluster().getId()); + assertEquals(1, vm.getTags().getItems().size()); + assertEquals(1, vm.getDiskAttachments().getItems().size()); + assertEquals(1, vm.getNics().getItems().size()); + assertEquals("acct-2", vm.getAccountId()); + assertEquals("ag-2", vm.getAffinityGroupId()); + assertEquals("ud-2", vm.getUserDataId()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/UserVmVOToCheckpointConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/UserVmVOToCheckpointConverterTest.java new file mode 100644 index 00000000000..7fb0396fc68 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/UserVmVOToCheckpointConverterTest.java @@ -0,0 +1,56 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.apache.cloudstack.veeam.api.dto.Checkpoint; +import org.junit.Test; + +public class UserVmVOToCheckpointConverterTest { + + @Test + public void testToCheckpoint_ReturnsNullWhenCheckpointIdMissing() { + assertNull(UserVmVOToCheckpointConverter.toCheckpoint(null, "10")); + assertNull(UserVmVOToCheckpointConverter.toCheckpoint("", "10")); + } + + @Test + public void testToCheckpoint_ParsesCreationTimeWhenProvided() { + final Checkpoint checkpoint = UserVmVOToCheckpointConverter.toCheckpoint("chk-1", "1700000000"); + + assertNotNull(checkpoint); + assertEquals("chk-1", checkpoint.getId()); + assertEquals("chk-1", checkpoint.getName()); + assertEquals(String.valueOf(1700000000L * 1000L), checkpoint.getCreationDate()); + assertEquals("created", checkpoint.getState()); + } + + @Test + public void testToCheckpoint_UsesNowWhenCreateTimeInvalid() { + final long before = System.currentTimeMillis(); + final Checkpoint checkpoint = UserVmVOToCheckpointConverter.toCheckpoint("chk-2", "not-a-number"); + final long after = System.currentTimeMillis(); + + final long creation = Long.parseLong(checkpoint.getCreationDate()); + assertTrue(creation >= before && creation <= after); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/VmSnapshotVOToSnapshotConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/VmSnapshotVOToSnapshotConverterTest.java new file mode 100644 index 00000000000..75a36c13a4d --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/VmSnapshotVOToSnapshotConverterTest.java @@ -0,0 +1,74 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Date; +import java.util.List; + +import org.apache.cloudstack.veeam.api.dto.Snapshot; +import org.junit.Test; + +import com.cloud.vm.snapshot.VMSnapshot; +import com.cloud.vm.snapshot.VMSnapshotVO; + +public class VmSnapshotVOToSnapshotConverterTest { + + @Test + public void testToSnapshot_MapsReadyDiskAndMemorySnapshot() { + final VMSnapshotVO vo = mock(VMSnapshotVO.class); + when(vo.getUuid()).thenReturn("snap-1"); + when(vo.getDescription()).thenReturn("desc"); + when(vo.getCreated()).thenReturn(new Date(1234L)); + when(vo.getType()).thenReturn(VMSnapshotVO.Type.DiskAndMemory); + when(vo.getState()).thenReturn(VMSnapshot.State.Ready); + + final Snapshot snapshot = VmSnapshotVOToSnapshotConverter.toSnapshot(vo, "vm-1"); + + assertEquals("snap-1", snapshot.getId()); + assertTrue(snapshot.getHref().contains("/api/vms/vm-1/snapshots/snap-1")); + assertEquals("vm-1", snapshot.getVm().getId()); + assertEquals("desc", snapshot.getDescription()); + assertEquals(Long.valueOf(1234L), snapshot.getDate()); + assertEquals("true", snapshot.getPersistMemorystate()); + assertEquals("ok", snapshot.getSnapshotStatus()); + assertEquals("link", snapshot.getActions().asMap().keySet().iterator().next()); + } + + @Test + public void testToSnapshot_MapsNonReadyToLockedAndToSnapshotList() { + final VMSnapshotVO vo = mock(VMSnapshotVO.class); + when(vo.getUuid()).thenReturn("snap-2"); + when(vo.getDescription()).thenReturn("desc2"); + when(vo.getCreated()).thenReturn(new Date(5678L)); + when(vo.getType()).thenReturn(VMSnapshotVO.Type.Disk); + when(vo.getState()).thenReturn(VMSnapshot.State.Creating); + + final Snapshot snapshot = VmSnapshotVOToSnapshotConverter.toSnapshot(vo, "vm-2"); + assertEquals("false", snapshot.getPersistMemorystate()); + assertEquals("locked", snapshot.getSnapshotStatus()); + + final List list = VmSnapshotVOToSnapshotConverter.toSnapshotList(List.of(vo), "vm-2"); + assertEquals(1, list.size()); + assertEquals("snap-2", list.get(0).getId()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverterTest.java new file mode 100644 index 00000000000..ae4d5961748 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/converter/VolumeJoinVOToDiskConverterTest.java @@ -0,0 +1,100 @@ +// 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.veeam.api.converter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.cloudstack.backup.Backup; +import org.apache.cloudstack.veeam.api.dto.Disk; +import org.apache.cloudstack.veeam.api.dto.DiskAttachment; +import org.junit.Test; + +import com.cloud.api.query.vo.VolumeJoinVO; +import com.cloud.storage.Storage; +import com.cloud.storage.Volume; + +public class VolumeJoinVOToDiskConverterTest { + + @Test + public void testToDisk_MapsCoreFieldsAndResolverOverridesActualSize() { + final VolumeJoinVO vol = mock(VolumeJoinVO.class); + when(vol.getUuid()).thenReturn("vol-1"); + when(vol.getVolumeType()).thenReturn(Volume.Type.ROOT); + when(vol.getName()).thenReturn("root-disk"); + when(vol.getSize()).thenReturn(1000L); + when(vol.getVolumeStoreSize()).thenReturn(500L); + when(vol.getFormat()).thenReturn(Storage.ImageFormat.RAW); + when(vol.getState()).thenReturn(Volume.State.Ready); + when(vol.getPath()).thenReturn("path-1"); + when(vol.getDiskOfferingUuid()).thenReturn("do-1"); + when(vol.getPoolUuid()).thenReturn("pool-1"); + + final Disk disk = VolumeJoinVOToDiskConverter.toDisk(vol, v -> 700L); + + assertEquals("vol-1", disk.getId()); + assertEquals("true", disk.getBootable()); + assertEquals("raw", disk.getFormat()); + assertEquals("ok", disk.getStatus()); + assertEquals("700", disk.getActualSize()); + assertNotNull(disk.getStorageDomains()); + assertTrue(disk.getHref().contains("/api/disks/vol-1")); + } + + @Test + public void testToDiskAttachment_MapsVmAndDisk() { + final VolumeJoinVO vol = mock(VolumeJoinVO.class); + when(vol.getUuid()).thenReturn("vol-2"); + when(vol.getVmUuid()).thenReturn("vm-2"); + when(vol.getVolumeType()).thenReturn(Volume.Type.DATADISK); + when(vol.getName()).thenReturn("data-disk"); + when(vol.getSize()).thenReturn(2048L); + when(vol.getVolumeStoreSize()).thenReturn(1024L); + when(vol.getFormat()).thenReturn(Storage.ImageFormat.QCOW2); + when(vol.getState()).thenReturn(Volume.State.Allocated); + when(vol.getPath()).thenReturn("path-2"); + when(vol.getDiskOfferingUuid()).thenReturn("do-2"); + when(vol.getPoolUuid()).thenReturn("pool-2"); + + final DiskAttachment da = VolumeJoinVOToDiskConverter.toDiskAttachment(vol, null); + + assertEquals("vol-2", da.getId()); + assertEquals("vm-2", da.getVm().getId()); + assertEquals("false", da.getBootable()); + assertEquals("virtio_scsi", da.getIface()); + assertEquals("vol-2", da.getDisk().getId()); + } + + @Test + public void testToDiskListFromVolumeInfos_MapsBootableByVolumeType() { + final Backup.VolumeInfo root = new Backup.VolumeInfo("root-id", "p1", Volume.Type.ROOT, 10L, 0L, "do", 0L, 0L); + final Backup.VolumeInfo data = new Backup.VolumeInfo("data-id", "p2", Volume.Type.DATADISK, 20L, 1L, "do", 0L, 0L); + + final List result = VolumeJoinVOToDiskConverter.toDiskListFromVolumeInfos(List.of(root, data)); + + assertEquals(2, result.size()); + assertEquals("true", result.get(0).getBootable()); + assertEquals("false", result.get(1).getBootable()); + assertEquals("20", result.get(1).getTotalSize()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java new file mode 100644 index 00000000000..16fbd02669c --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/api/request/ListQueryTest.java @@ -0,0 +1,92 @@ +// 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.veeam.api.request; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; + +public class ListQueryTest { + + @Test + public void testFromRequest_WithNoParametersReturnsDefaults() { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameterMap()).thenReturn(Collections.emptyMap()); + + final ListQuery query = ListQuery.fromRequest(request); + + assertFalse(query.isAllContent()); + assertNull(query.getLimit()); + assertNull(query.getOffset()); + assertFalse(query.followContains("tags")); + } + + @Test + public void testFromRequest_ParsesAllContentMaxAndFollow() { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameterMap()).thenReturn(Map.of( + "all_content", new String[]{"true"}, + "max", new String[]{"25"}, + "follow", new String[]{"tags, disk_attachments.disk, ,nics.reporteddevices"} + )); + when(request.getParameter("all_content")).thenReturn("true"); + when(request.getParameter("max")).thenReturn("25"); + when(request.getParameter("follow")).thenReturn("tags, disk_attachments.disk, ,nics.reporteddevices"); + when(request.getParameter("search")).thenReturn(null); + + final ListQuery query = ListQuery.fromRequest(request); + + assertTrue(query.isAllContent()); + assertTrue(query.followContains("tags")); + assertTrue(query.followContains("disk_attachments.disk")); + assertTrue(query.followContains("nics.reporteddevices")); + } + + @Test + public void testFromRequest_SearchParserIgnoresNonEqualsAndUsesPageValueAsMaxCurrentBehavior() { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameterMap()).thenReturn(Map.of("search", new String[]{"name=vm and page=3 and status!=down and x>=1"})); + when(request.getParameter("all_content")).thenReturn(null); + when(request.getParameter("max")).thenReturn(null); + when(request.getParameter("follow")).thenReturn(null); + when(request.getParameter("search")).thenReturn("name=vm and page=3 and status!=down and x>=1"); + + final ListQuery query = ListQuery.fromRequest(request); + + // Document existing behavior: when search contains page=..., max is set from it. + org.junit.Assert.assertEquals(Long.valueOf(3L), query.getLimit()); + } + + @Test + public void testGetOffset_UsesPageAndMax() { + final ListQuery query = new ListQuery(); + query.page = 3L; + query.max = 10L; + + org.junit.Assert.assertEquals(Long.valueOf(20L), query.getOffset()); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/filter/AllowedClientCidrsFilterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/filter/AllowedClientCidrsFilterTest.java new file mode 100644 index 00000000000..d14d1291dc1 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/filter/AllowedClientCidrsFilterTest.java @@ -0,0 +1,130 @@ +// 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.veeam.filter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.cloudstack.veeam.VeeamControlService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +@RunWith(MockitoJUnitRunner.class) +public class AllowedClientCidrsFilterTest { + + @Mock + private VeeamControlService veeamControlService; + + @Mock + private FilterChain chain; + + @Test + public void testDoFilterAllowsNonHttpRequestsToPassThrough() throws Exception { + final AllowedClientCidrsFilter filter = new AllowedClientCidrsFilter(veeamControlService); + final ServletRequest request = mock(ServletRequest.class); + final ServletResponse response = mock(ServletResponse.class); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + verifyNoInteractions(veeamControlService); + } + + @Test + public void testDoFilterRejectsWhenServiceIsUnavailable() throws Exception { + final AllowedClientCidrsFilter filter = new AllowedClientCidrsFilter(null); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + filter.doFilter(request, response, chain); + + assertEquals(503, response.getStatus()); + assertEquals("Service Unavailable", response.getErrorMessage()); + verifyNoInteractions(chain); + } + + @Test + public void testDoFilterAllowsRequestWhenNoCidrsConfigured() throws Exception { + final AllowedClientCidrsFilter filter = new AllowedClientCidrsFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + when(veeamControlService.getAllowedClientCidrs()).thenReturn(Collections.emptyList()); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + } + + @Test + public void testDoFilterAllowsRequestFromConfiguredCidr() throws Exception { + final AllowedClientCidrsFilter filter = new AllowedClientCidrsFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setRemoteAddr("192.168.10.25"); + when(veeamControlService.getAllowedClientCidrs()).thenReturn(List.of("192.168.10.0/24")); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + assertEquals(200, response.getStatus()); + } + + @Test + public void testDoFilterRejectsRequestOutsideConfiguredCidrs() throws Exception { + final AllowedClientCidrsFilter filter = new AllowedClientCidrsFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setRemoteAddr("10.10.10.10"); + when(veeamControlService.getAllowedClientCidrs()).thenReturn(List.of("192.168.10.0/24")); + + filter.doFilter(request, response, chain); + + assertEquals(403, response.getStatus()); + assertEquals("Forbidden", response.getErrorMessage()); + verifyNoInteractions(chain); + } + + @Test + public void testDoFilterRejectsMalformedRemoteAddress() throws Exception { + final AllowedClientCidrsFilter filter = new AllowedClientCidrsFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setRemoteAddr("not-a-valid-ip-address"); + when(veeamControlService.getAllowedClientCidrs()).thenReturn(List.of("192.168.10.0/24")); + + filter.doFilter(request, response, chain); + + assertEquals(403, response.getStatus()); + assertEquals("Forbidden", response.getErrorMessage()); + verifyNoInteractions(chain); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/filter/BearerOrBasicAuthFilterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/filter/BearerOrBasicAuthFilterTest.java new file mode 100644 index 00000000000..ba25da38e3e --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/filter/BearerOrBasicAuthFilterTest.java @@ -0,0 +1,129 @@ +// 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.veeam.filter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.servlet.FilterChain; + +import org.apache.cloudstack.veeam.VeeamControlService; +import org.apache.cloudstack.veeam.utils.JwtUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +@RunWith(MockitoJUnitRunner.class) +public class BearerOrBasicAuthFilterTest { + + private static final String SECRET = "very-secret"; + + @Mock + private VeeamControlService veeamControlService; + + @Mock + private FilterChain chain; + + @Test + public void testDoFilterRejectsMissingAuthorizationWithJsonPayload() throws Exception { + final BearerOrBasicAuthFilter filter = new BearerOrBasicAuthFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.addHeader("Accept", "application/json"); + + filter.doFilter(request, response, chain); + + assertEquals(401, response.getStatus()); + assertEquals("application/json; charset=UTF-8", response.getContentType()); + assertNotNull(response.getHeader("WWW-Authenticate")); + assertTrue(response.getHeader("WWW-Authenticate").contains("error=\"invalid_token\"")); + assertTrue(response.getContentAsString().contains("\"error\":\"invalid_token\"")); + assertTrue(response.getContentAsString().contains("Missing Authorization")); + verifyNoInteractions(chain); + } + + @Test + public void testDoFilterRejectsInvalidBearerToken() throws Exception { + final BearerOrBasicAuthFilter filter = new BearerOrBasicAuthFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.addHeader("Accept", "application/json"); + request.addHeader("Authorization", "Bearer not-a-jwt"); + + filter.doFilter(request, response, chain); + + assertEquals(401, response.getStatus()); + assertTrue(response.getContentAsString().contains("Invalid or expired token")); + verifyNoInteractions(chain); + } + + @Test + public void testDoFilterAllowsValidBearerTokenWithRequiredScopes() throws Exception { + final BearerOrBasicAuthFilter filter = new BearerOrBasicAuthFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + final String token = JwtUtil.issueHs256Jwt("service-user", "ovirt-app-admin ovirt-app-portal", 60L, SECRET); + request.addHeader("Authorization", "Bearer " + token); + when(veeamControlService.getHmacSecret()).thenReturn(SECRET); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + } + + @Test + public void testDoFilterAllowsValidBasicCredentials() throws Exception { + final BearerOrBasicAuthFilter filter = new BearerOrBasicAuthFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + final String credentials = Base64.getEncoder().encodeToString("veeam:secret".getBytes(StandardCharsets.UTF_8)); + request.addHeader("Authorization", "Basic " + credentials); + when(veeamControlService.validateCredentials("veeam", "secret")).thenReturn(true); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + } + + @Test + public void testDoFilterRejectsInvalidBasicCredentialsWithHtmlPayload() throws Exception { + final BearerOrBasicAuthFilter filter = new BearerOrBasicAuthFilter(veeamControlService); + final MockHttpServletRequest request = new MockHttpServletRequest(); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.addHeader("Authorization", "Basic !!!not-base64!!!"); + + filter.doFilter(request, response, chain); + + assertEquals(401, response.getStatus()); + assertEquals("text/html; charset=UTF-8", response.getContentType()); + assertTrue(response.getContentAsString().contains("Unauthorized")); + assertNotNull(response.getHeader("WWW-Authenticate")); + assertTrue(response.getHeader("WWW-Authenticate").contains("invalid_client")); + verifyNoInteractions(chain); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/services/PkiResourceRouteHandlerTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/services/PkiResourceRouteHandlerTest.java new file mode 100644 index 00000000000..cbae8716453 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/services/PkiResourceRouteHandlerTest.java @@ -0,0 +1,127 @@ +// 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.veeam.services; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.veeam.VeeamControlServlet; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +@RunWith(MockitoJUnitRunner.class) +public class PkiResourceRouteHandlerTest { + + @Test + public void testCanHandleSanitizesQueryParameters() { + final PkiResourceRouteHandler handler = new PkiResourceRouteHandler(); + + assertTrue(handler.canHandle("GET", "/services/pki-resource?resource=ca-certificate")); + } + + @Test + public void testHandleReturnsCertificateDownload() throws Exception { + final PkiResourceRouteHandler handler = new PkiResourceRouteHandler(); + final CAManager caManager = mock(CAManager.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/pki-resource"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + final String certificate = "-----BEGIN CERTIFICATE-----\nveeam\n-----END CERTIFICATE-----\n"; + handler.caManager = caManager; + when(caManager.getCaCertificate(null)).thenReturn(certificate); + + handler.handle(request, response, "/services/pki-resource", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(200, response.getStatus()); + assertEquals("no-store", response.getHeader("Cache-Control")); + assertEquals("attachment; filename=\"pki-resource.cer\"", response.getHeader("Content-Disposition")); + assertEquals("application/x-x509-ca-cert; charset=ISO-8859-1", response.getContentType()); + assertArrayEquals(certificate.getBytes(StandardCharsets.ISO_8859_1), response.getContentAsByteArray()); + } + + @Test + public void testHandleRejectsUnsupportedResource() throws Exception { + final PkiResourceRouteHandler handler = new PkiResourceRouteHandler(); + handler.caManager = mock(CAManager.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/pki-resource"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("resource", "unsupported"); + + handler.handle(request, response, "/services/pki-resource", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(400, response.getStatus()); + assertEquals("Unsupported resource", response.getErrorMessage()); + } + + @Test + public void testHandleRejectsUnsupportedFormat() throws Exception { + final PkiResourceRouteHandler handler = new PkiResourceRouteHandler(); + handler.caManager = mock(CAManager.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/pki-resource"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("format", "PEM"); + + handler.handle(request, response, "/services/pki-resource", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(400, response.getStatus()); + assertEquals("Unsupported format", response.getErrorMessage()); + } + + @Test + public void testHandleRejectsEmptyCertificateData() throws Exception { + final PkiResourceRouteHandler handler = new PkiResourceRouteHandler(); + final CAManager caManager = mock(CAManager.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/pki-resource"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + handler.caManager = caManager; + when(caManager.getCaCertificate(null)).thenReturn(""); + + handler.handle(request, response, "/services/pki-resource", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(500, response.getStatus()); + assertEquals("No certificate data available", response.getErrorMessage()); + } + + @Test + public void testHandleReturnsNotFoundForUnknownPath() throws Exception { + final PkiResourceRouteHandler handler = new PkiResourceRouteHandler(); + handler.caManager = mock(CAManager.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/services/pki-resource/unknown"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + handler.handle(request, response, "/services/pki-resource/unknown", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(404, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"reason\":\"Not found\"")); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/sso/SsoServiceTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/sso/SsoServiceTest.java new file mode 100644 index 00000000000..d6c2c87fd03 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/sso/SsoServiceTest.java @@ -0,0 +1,219 @@ +// 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.veeam.sso; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; + +import org.apache.cloudstack.veeam.VeeamControlService; +import org.apache.cloudstack.veeam.VeeamControlServlet; +import org.apache.cloudstack.veeam.utils.Mapper; +import org.apache.cloudstack.veeam.utils.Negotiation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.fasterxml.jackson.core.type.TypeReference; + +@RunWith(MockitoJUnitRunner.class) +public class SsoServiceTest { + + private final Mapper mapper = new Mapper(); + + @Test + public void testCanHandleSanitizesQueryParameters() { + final SsoService service = new SsoService(); + + assertTrue(service.canHandle("POST", "/sso/oauth/token?scope=abc")); + } + + @Test + public void testHandleReturnsNotFoundForUnknownPath() throws Exception { + final SsoService service = new SsoService(); + service.veeamControlService = mock(VeeamControlService.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/sso/unknown"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + service.handle(request, response, "/sso/unknown", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(404, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"reason\":\"Not found\"")); + } + + @Test + public void testHandleTokenRejectsNonPostMethod() throws Exception { + final SsoService service = new SsoService(); + service.veeamControlService = mock(VeeamControlService.class); + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(405, response.getStatus()); + assertEquals("POST", response.getHeader("Allow")); + assertTrue(response.getContentAsString().contains("method_not_allowed")); + } + + @Test + public void testHandleTokenRejectsMissingGrantType() throws Exception { + final SsoService service = new SsoService(); + service.veeamControlService = mock(VeeamControlService.class); + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(400, response.getStatus()); + assertTrue(response.getContentAsString().contains("Missing parameter: grant_type")); + } + + @Test + public void testHandleTokenRejectsUnsupportedGrantType() throws Exception { + final SsoService service = new SsoService(); + service.veeamControlService = mock(VeeamControlService.class); + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("grant_type", "client_credentials"); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(400, response.getStatus()); + assertTrue(response.getContentAsString().contains("unsupported_grant_type")); + } + + @Test + public void testHandleTokenRejectsMissingCredentials() throws Exception { + final SsoService service = new SsoService(); + service.veeamControlService = mock(VeeamControlService.class); + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("grant_type", "password"); + request.setParameter("username", "veeam"); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(400, response.getStatus()); + assertTrue(response.getContentAsString().contains("Missing username/password")); + } + + @Test + public void testHandleTokenRejectsInvalidCredentials() throws Exception { + final SsoService service = new SsoService(); + final VeeamControlService controlService = mock(VeeamControlService.class); + service.veeamControlService = controlService; + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("grant_type", "password"); + request.setParameter("username", "veeam"); + request.setParameter("password", "wrong"); + when(controlService.validateCredentials("veeam", "wrong")).thenReturn(false); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(401, response.getStatus()); + assertTrue(response.getContentAsString().contains("invalid_grant")); + } + + @Test + public void testHandleTokenReturnsServerErrorWhenTokenIssuanceFails() throws Exception { + final SsoService service = new SsoService(); + final VeeamControlService controlService = mock(VeeamControlService.class); + service.veeamControlService = controlService; + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("grant_type", "password"); + request.setParameter("username", "veeam"); + request.setParameter("password", "secret"); + when(controlService.validateCredentials("veeam", "secret")).thenReturn(true); + when(controlService.getHmacSecret()).thenReturn(null); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(500, response.getStatus()); + assertTrue(response.getContentAsString().contains("Failed to issue token")); + } + + @Test + public void testHandleTokenIssuesTokenWithDefaultScopes() throws Exception { + final SsoService service = new SsoService(); + final VeeamControlService controlService = mock(VeeamControlService.class); + service.veeamControlService = controlService; + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("grant_type", "password"); + request.setParameter("username", "veeam"); + request.setParameter("password", "secret"); + when(controlService.validateCredentials("veeam", "secret")).thenReturn(true); + when(controlService.getHmacSecret()).thenReturn("very-secret"); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(200, response.getStatus()); + + final Map payload = mapper.jsonMapper().readValue(response.getContentAsByteArray(), new TypeReference<>() { + }); + assertEquals("bearer", payload.get("token_type")); + assertEquals(3600, ((Number) payload.get("expires_in")).intValue()); + assertEquals(String.join(" ", SsoService.REQUIRED_SCOPES), payload.get("scope")); + + final String accessToken = (String) payload.get("access_token"); + final String jwtPayload = new String(Base64.getUrlDecoder().decode(accessToken.split("\\.")[1]), StandardCharsets.UTF_8); + final Map jwtClaims = mapper.jsonMapper().readValue(jwtPayload, new TypeReference<>() { + }); + assertEquals("veeam", jwtClaims.get("sub")); + assertEquals(String.join(" ", SsoService.REQUIRED_SCOPES), jwtClaims.get("scope")); + } + + @Test + public void testHandleTokenHonorsCustomScope() throws Exception { + final SsoService service = new SsoService(); + final VeeamControlService controlService = mock(VeeamControlService.class); + service.veeamControlService = controlService; + final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/sso/oauth/token"); + final MockHttpServletResponse response = new MockHttpServletResponse(); + request.setParameter("grant_type", "password"); + request.setParameter("username", "veeam"); + request.setParameter("password", "secret"); + request.setParameter("scope", "custom-scope"); + when(controlService.validateCredentials("veeam", "secret")).thenReturn(true); + when(controlService.getHmacSecret()).thenReturn("very-secret"); + + service.handle(request, response, "/sso/oauth/token", Negotiation.OutFormat.JSON, + new VeeamControlServlet(Collections.emptyList())); + + assertEquals(200, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"scope\":\"custom-scope\"")); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/DataUtilTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/DataUtilTest.java new file mode 100644 index 00000000000..f2d6b72eb47 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/DataUtilTest.java @@ -0,0 +1,57 @@ +// 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.veeam.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +public class DataUtilTest { + + @Test + public void testB64Url_UsesUrlSafeAlphabetAndNoPadding() { + final String encoded = DataUtil.b64Url(new byte[]{(byte)0xfb, (byte)0xff}); + assertEquals("-_8", encoded); + } + + @Test + public void testJsonEscape_NullAndEscapedCharacters() { + assertEquals("", DataUtil.jsonEscape(null)); + assertEquals("a\\\\b\\\"c", DataUtil.jsonEscape("a\\b\"c")); + } + + @Test + public void testConstantTimeEquals_StringOverload() { + assertTrue(DataUtil.constantTimeEquals("abc", "abc")); + assertFalse(DataUtil.constantTimeEquals("abc", "abd")); + assertFalse(DataUtil.constantTimeEquals(null, "abc")); + assertFalse(DataUtil.constantTimeEquals("abc", null)); + } + + @Test + public void testConstantTimeEquals_ByteArrayOverload() { + final byte[] left = "sample".getBytes(StandardCharsets.UTF_8); + assertTrue(DataUtil.constantTimeEquals(left, "sample".getBytes(StandardCharsets.UTF_8))); + assertFalse(DataUtil.constantTimeEquals(left, "samples".getBytes(StandardCharsets.UTF_8))); + assertFalse(DataUtil.constantTimeEquals(left, "samplE".getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/JwtUtilTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/JwtUtilTest.java new file mode 100644 index 00000000000..255eec3744b --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/JwtUtilTest.java @@ -0,0 +1,79 @@ +// 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.veeam.utils; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; + +import org.junit.Test; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class JwtUtilTest { + + @Test + public void testHmacSha256_KnownVector() throws Exception { + final byte[] actual = JwtUtil.hmacSha256("data".getBytes(StandardCharsets.UTF_8), "key".getBytes(StandardCharsets.UTF_8)); + final byte[] expected = hexToBytes("5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0"); + assertArrayEquals(expected, actual); + } + + @Test + public void testIssueHs256Jwt_BuildsValidTokenAndClaims() throws Exception { + final long before = Instant.now().getEpochSecond(); + final String token = JwtUtil.issueHs256Jwt("sub-1", "scope-a", 120L, "very-secret"); + final long after = Instant.now().getEpochSecond(); + + final String[] parts = token.split("\\."); + assertEquals(3, parts.length); + + final JsonObject header = JsonParser.parseString(new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8)).getAsJsonObject(); + final JsonObject payload = JsonParser.parseString(new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8)).getAsJsonObject(); + + assertEquals("HS256", header.get("alg").getAsString()); + assertEquals("JWT", header.get("typ").getAsString()); + assertEquals(JwtUtil.ISSUER, payload.get("iss").getAsString()); + assertEquals("sub-1", payload.get("sub").getAsString()); + assertEquals("scope-a", payload.get("scope").getAsString()); + + final long iat = payload.get("iat").getAsLong(); + final long exp = payload.get("exp").getAsLong(); + assertTrue(iat >= before && iat <= after); + assertEquals(120L, exp - iat); + + final byte[] expectedSig = JwtUtil.hmacSha256((parts[0] + "." + parts[1]).getBytes(StandardCharsets.UTF_8), "very-secret".getBytes(StandardCharsets.UTF_8)); + final byte[] actualSig = Base64.getUrlDecoder().decode(parts[2]); + assertArrayEquals(expectedSig, actualSig); + } + + private static byte[] hexToBytes(final String hex) { + final byte[] out = new byte[hex.length() / 2]; + for (int i = 0; i < out.length; i++) { + final int hi = Character.digit(hex.charAt(i * 2), 16); + final int lo = Character.digit(hex.charAt(i * 2 + 1), 16); + out[i] = (byte)((hi << 4) + lo); + } + return out; + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/MapperTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/MapperTest.java new file mode 100644 index 00000000000..d444386d066 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/MapperTest.java @@ -0,0 +1,72 @@ +// 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.veeam.utils; + +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 org.junit.Test; + +public class MapperTest { + + @Test + public void testToJson_UsesSnakeCaseAndOmitsNulls() throws Exception { + final Mapper mapper = new Mapper(); + final String json = mapper.toJson(new Sample("John", "Doe", null)); + + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Doe\"")); + assertFalse(json.contains("optional_field")); + } + + @Test + public void testToXml_UsesSnakeCaseAndOmitsNulls() throws Exception { + final Mapper mapper = new Mapper(); + final String xml = mapper.toXml(new Sample("John", "Doe", null)); + + assertTrue(xml.contains("John")); + assertTrue(xml.contains("Doe")); + assertFalse(xml.contains("optional_field")); + } + + @Test + public void testJsonMapper_IgnoresUnknownProperties() throws Exception { + final Mapper mapper = new Mapper(); + final Sample sample = mapper.jsonMapper().readValue("{\"first_name\":\"Alice\",\"unknown\":\"x\"}", Sample.class); + + assertNotNull(sample); + assertEquals("Alice", sample.firstName); + } + + static class Sample { + public String firstName; + public String lastName; + public String optionalField; + + Sample() { + } + + Sample(final String firstName, final String lastName, final String optionalField) { + this.firstName = firstName; + this.lastName = lastName; + this.optionalField = optionalField; + } + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/NegotiationTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/NegotiationTest.java new file mode 100644 index 00000000000..fb08f14b24e --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/NegotiationTest.java @@ -0,0 +1,66 @@ +// 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.veeam.utils; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Test; + +public class NegotiationTest { + + @Test + public void testResponseFormat_DefaultsToXmlForNullBlankWildcardAndUnknown() { + final HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader("Accept")).thenReturn(null); + assertEquals(Negotiation.OutFormat.XML, Negotiation.responseFormat(request)); + + when(request.getHeader("Accept")).thenReturn(" "); + assertEquals(Negotiation.OutFormat.XML, Negotiation.responseFormat(request)); + + when(request.getHeader("Accept")).thenReturn("*/*"); + assertEquals(Negotiation.OutFormat.XML, Negotiation.responseFormat(request)); + + when(request.getHeader("Accept")).thenReturn("application/octet-stream"); + assertEquals(Negotiation.OutFormat.XML, Negotiation.responseFormat(request)); + } + + @Test + public void testResponseFormat_ResolvesJsonAndXmlMediaTypesCaseInsensitively() { + final HttpServletRequest request = mock(HttpServletRequest.class); + + when(request.getHeader("Accept")).thenReturn("Application/JSON"); + assertEquals(Negotiation.OutFormat.JSON, Negotiation.responseFormat(request)); + + when(request.getHeader("Accept")).thenReturn("application/xml"); + assertEquals(Negotiation.OutFormat.XML, Negotiation.responseFormat(request)); + + when(request.getHeader("Accept")).thenReturn("text/xml"); + assertEquals(Negotiation.OutFormat.XML, Negotiation.responseFormat(request)); + } + + @Test + public void testContentType_ReturnsMimeTypeForEachFormat() { + assertEquals("application/json", Negotiation.contentType(Negotiation.OutFormat.JSON)); + assertEquals("application/xml", Negotiation.contentType(Negotiation.OutFormat.XML)); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/PathUtilTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/PathUtilTest.java new file mode 100644 index 00000000000..f069c724c70 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/PathUtilTest.java @@ -0,0 +1,54 @@ +// 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.veeam.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.List; + +import org.junit.Test; + +public class PathUtilTest { + + @Test + public void testExtractIdAndSubPath_ReturnsNullForBlankOrInvalidPath() { + assertNull(PathUtil.extractIdAndSubPath(null, "/api/datacenters")); + assertNull(PathUtil.extractIdAndSubPath(" ", "/api/datacenters")); + assertNull(PathUtil.extractIdAndSubPath("api/datacenters/123", "/api/datacenters")); + assertNull(PathUtil.extractIdAndSubPath("/api/datacenters", "/api/datacenters")); + } + + @Test + public void testExtractIdAndSubPath_RemovesBaseRouteAndReturnsSegments() { + final List parts = PathUtil.extractIdAndSubPath("/api/datacenters/123/sub/path", "/api/datacenters"); + assertEquals(List.of("123", "sub", "path"), parts); + } + + @Test + public void testExtractIdAndSubPath_HandlesTrailingSlashBaseAndRepeatedSlashes() { + final List parts = PathUtil.extractIdAndSubPath("/api/datacenters//123///child/", "/api/datacenters/"); + assertEquals(List.of("123", "child"), parts); + } + + @Test + public void testExtractIdAndSubPath_WithoutBaseRouteUsesPathDirectly() { + final List parts = PathUtil.extractIdAndSubPath("/id-1/sub", ""); + assertEquals(List.of("id-1", "sub"), parts); + } +} diff --git a/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/ResponseWriterTest.java b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/ResponseWriterTest.java new file mode 100644 index 00000000000..241d92f7c93 --- /dev/null +++ b/plugins/integrations/veeam-control-service/src/test/java/org/apache/cloudstack/veeam/utils/ResponseWriterTest.java @@ -0,0 +1,116 @@ +// 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.veeam.utils; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; + +public class ResponseWriterTest { + + @Test + public void testWrite_NullBodySetsStatusAndZeroContentLength() throws Exception { + final Mapper mapper = new Mapper(); + final ResponseWriter responseWriter = new ResponseWriter(mapper); + final HttpServletResponse response = mock(HttpServletResponse.class); + + responseWriter.write(response, 204, null, Negotiation.OutFormat.XML); + + verify(response).setStatus(204); + verify(response).setContentLength(0); + verify(response, never()).getWriter(); + } + + @Test + public void testWrite_JsonBodyWritesPayloadAndHeaders() throws Exception { + final ResponseWriter responseWriter = new ResponseWriter(new Mapper()); + final HttpServletResponse response = mock(HttpServletResponse.class); + final StringWriter sink = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(sink)); + + responseWriter.write(response, 200, new Payload("item-1"), Negotiation.OutFormat.JSON); + + verify(response).setStatus(200); + verify(response).setHeader("Content-Type", "application/json"); + assertTrue(sink.toString().contains("\"name\":\"item-1\"")); + } + + @Test + public void testWrite_XmlBodyWritesPayloadAndHeaders() throws Exception { + final ResponseWriter responseWriter = new ResponseWriter(new Mapper()); + final HttpServletResponse response = mock(HttpServletResponse.class); + final StringWriter sink = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(sink)); + + responseWriter.write(response, 200, new Payload("item-2"), Negotiation.OutFormat.XML); + + verify(response).setHeader("Content-Type", "application/xml"); + assertTrue(sink.toString().contains("item-2")); + } + + @Test + public void testWrite_WhenMappingFailsReturnsInternalServerError() throws Exception { + final Mapper mapper = mock(Mapper.class); + doThrow(new RuntimeException("boom")).when(mapper).toJson(org.mockito.ArgumentMatchers.any()); + + final ResponseWriter responseWriter = new ResponseWriter(mapper); + final HttpServletResponse response = mock(HttpServletResponse.class); + final StringWriter sink = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(sink)); + + responseWriter.write(response, 200, new Payload("ignored"), Negotiation.OutFormat.JSON); + + verify(response).setStatus(500); + verify(response).setHeader("Content-Type", "text/plain"); + assertTrue(sink.toString().contains("Internal Server Error")); + verify(response, never()).setContentLength(anyInt()); + } + + @Test + public void testWriteFault_JsonWritesFaultStructure() throws Exception { + final ResponseWriter responseWriter = new ResponseWriter(new Mapper()); + final HttpServletResponse response = mock(HttpServletResponse.class); + final StringWriter sink = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(sink)); + + responseWriter.writeFault(response, 404, "Not Found", "missing vm", Negotiation.OutFormat.JSON); + + verify(response).setStatus(404); + assertTrue(sink.toString().contains("\"reason\":\"Not Found\"")); + assertTrue(sink.toString().contains("\"detail\":\"missing vm\"")); + } + + static class Payload { + public String name; + + Payload(final String name) { + this.name = name; + } + } +}