From 3bf8efa0365ecca6eb5544c96ec12d28162a4097 Mon Sep 17 00:00:00 2001
From: Ondrej Holecek <oholecek@suse.com>
Date: Tue, 12 Nov 2024 21:26:53 +0100
Subject: [PATCH] Add provisionSystem API with proxy option

Using web ui it is possible to schedule system provisioning through
specific proxy. This is not possible using any API call.
This commit adds this missing provisionSystem API call and related unit
tests.
Automatic proxy detection based on request headers is preserved.
---
 .../frontend/xmlrpc/system/SystemHandler.java | 129 ++++++++-
 .../test/SystemHandlerProvisioningTest.java   | 266 ++++++++++++++++++
 .../testing/RhnMockHttpServletRequest.java    |  10 +
 ...hanges.oholecek.add_proxy_api_provisioning |   2 +
 4 files changed, 402 insertions(+), 5 deletions(-)
 create mode 100644 java/code/src/com/redhat/rhn/frontend/xmlrpc/system/test/SystemHandlerProvisioningTest.java
 create mode 100644 java/spacewalk-java.changes.oholecek.add_proxy_api_provisioning
#L3PD: bsc#1237544

diff --git a/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/SystemHandler.java b/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/SystemHandler.java
index 905face6792..1b9824a29a9 100644
--- a/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/SystemHandler.java
+++ b/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/SystemHandler.java
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2024 SUSE LLC
  * Copyright (c) 2009--2017 Red Hat, Inc.
  *
  * This software is licensed to you under the GNU General Public License,
@@ -2931,7 +2932,7 @@ public int provisionVirtualGuest(User loggedInUser, Integer sid, String guestNam
      * @param loggedInUser The current user
      * @param sid of the system to be provisioned
      * @param profileName of Profile to be used.
-     * @return Returns 1 if successful, exception otherwise
+     * @return Returns id of the action if successful, exception otherwise
      * @throws FaultException A FaultException is thrown if the server corresponding to
      * id cannot be found or profile is not found.
      *
@@ -2955,7 +2956,7 @@ public int provisionSystem(User loggedInUser, Integer sid, String profileName)
      * @param request the spark request
      * @param sid of the system to be provisioned
      * @param profileName of Profile to be used.
-     * @return Returns 1 if successful, exception otherwise
+     * @return Returns id of the action if successful, exception otherwise
      * @throws FaultException A FaultException is thrown if the server corresponding to
      * id cannot be found or profile is not found.
      *
@@ -2972,6 +2973,58 @@ public int provisionSystem(User loggedInUser, HttpServletRequest request, Intege
         return provisionSystem(loggedInUser, request, sid, profileName, new Date());
     }
 
+    /**
+     * Provision a system using the specified kickstart/autoinstallation profile.
+     *
+     * @param loggedInUser The current user
+     * @param sid of the system to be provisioned
+     * @param proxy ID of the proxy to use
+     * @param profileName of Profile to be used
+     * @return Returns id of the action if successful, exception otherwise
+     * @throws FaultException A FaultException is thrown if the server corresponding to
+     * id cannot be found or profile is not found.
+     *
+     * @apidoc.doc Provision a system using the specified kickstart/autoinstallation profile.
+     * @apidoc.param #session_key()
+     * @apidoc.param #param_desc("int", "sid", "ID of the system to be provisioned.")
+     * @apidoc.param #param_desc("int", "proxy", "ID of the proxy to use.")
+     * @apidoc.param #param_desc("string", "profileName", "Profile to use.")
+     * @apidoc.returntype #param_desc("int", "id", "ID of the action scheduled, otherwise exception thrown
+     * on error")
+     */
+    @ApiIgnore(ApiType.HTTP)
+    public int provisionSystem(User loggedInUser, Integer sid, Integer proxy, String profileName)
+            throws FaultException {
+        return provisionSystem(loggedInUser, RhnXmlRpcServer.getRequest(), sid, proxy, profileName, new Date());
+    }
+
+    /**
+     * Provision a system using the specified kickstart/autoinstallation profile.
+     *
+     * @param loggedInUser The current user
+     * @param request the spark request
+     * @param sid of the system to be provisioned
+     * @param proxy ID of the proxy to use
+     * @param profileName of Profile to be used.
+     * @return Returns id of the action if successful, exception otherwise
+     * @throws FaultException A FaultException is thrown if the server corresponding to
+     * id cannot be found or profile is not found.
+     *
+     * @apidoc.doc Provision a system using the specified kickstart/autoinstallation profile.
+     * @apidoc.param #session_key()
+     * @apidoc.param #param_desc("int", "sid", "ID of the system to be provisioned.")
+     * @apidoc.param #param_desc("int", "proxy", "ID of the proxy to use.")
+     * @apidoc.param #param_desc("string", "profileName", "Profile to use.")
+     * @apidoc.returntype #param_desc("int", "id", "ID of the action scheduled, otherwise exception thrown
+     * on error")
+     */
+    @ApiIgnore(ApiType.XMLRPC)
+    public int provisionSystem(User loggedInUser, HttpServletRequest request, Integer sid, Integer proxy,
+                               String profileName)
+            throws FaultException {
+        return provisionSystem(loggedInUser, request, sid, proxy, profileName, new Date());
+    }
+
     /**
      * Provision a system using the specified kickstart/autoinstallation profile at specified time.
      *
@@ -2979,7 +3032,7 @@ public int provisionSystem(User loggedInUser, HttpServletRequest request, Intege
      * @param sid of the system to be provisioned
      * @param profileName of Profile to be used.
      * @param earliestDate when the autoinstallation needs to be scheduled
-     * @return Returns 1 if successful, exception otherwise
+     * @return Returns id of the action if successful, exception otherwise
      * @throws FaultException A FaultException is thrown if the server corresponding to
      * id cannot be found or profile is not found.
      *
@@ -3006,7 +3059,7 @@ public int provisionSystem(User loggedInUser, Integer sid,
      * @param sid of the system to be provisioned
      * @param profileName of Profile to be used.
      * @param earliestDate when the autoinstallation needs to be scheduled
-     * @return Returns 1 if successful, exception otherwise
+     * @return Returns id of the action if successful, exception otherwise
      * @throws FaultException A FaultException is thrown if the server corresponding to
      * id cannot be found or profile is not found.
      *
@@ -3022,6 +3075,63 @@ public int provisionSystem(User loggedInUser, Integer sid,
     public int provisionSystem(User loggedInUser, HttpServletRequest request, Integer sid,
                                 String profileName, Date earliestDate)
             throws FaultException {
+        return provisionSystem(loggedInUser, request , sid, null, profileName, earliestDate);
+    }
+
+    /**
+     * Provision a system using the specified kickstart/autoinstallation profile at specified time.
+     *
+     * @param loggedInUser The current user
+     * @param sid of the system to be provisioned
+     * @param proxy ID of the proxy to use
+     * @param profileName of Profile to be used.
+     * @param earliestDate when the autoinstallation needs to be scheduled
+     * @return Returns id of the action if successful, exception otherwise
+     * @throws FaultException A FaultException is thrown if the server corresponding to
+     * id cannot be found or profile is not found.
+     *
+     * @apidoc.doc Provision a system using the specified kickstart/autoinstallation profile.
+     * @apidoc.param #session_key()
+     * @apidoc.param #param_desc("int", "sid", "ID of the system to be provisioned.")
+     * @apidoc.param #param_desc("int", "proxy", "ID of the proxy to use")
+     * @apidoc.param #param_desc("string", "profileName", "Profile to use.")
+     * @apidoc.param #param("$date", "earliestDate")
+     * @apidoc.returntype #param_desc("int", "id", "ID of the action scheduled, otherwise exception thrown
+     * on error")
+     */
+    @ApiIgnore(ApiType.HTTP)
+    public int provisionSystem(User loggedInUser, Integer sid,
+                               Integer proxy, String profileName, Date earliestDate)
+            throws FaultException {
+        HttpServletRequest request = RhnXmlRpcServer.getRequest();
+        return provisionSystem(loggedInUser, request, sid, proxy, profileName, earliestDate);
+    }
+    /**
+     * Provision a system using the specified kickstart/autoinstallation profile at specified time.
+     *
+     * @param loggedInUser The current user
+     * @param request the request
+     * @param sid of the system to be provisioned
+     * @param proxy ID of the proxy to use
+     * @param profileName of Profile to be used.
+     * @param earliestDate when the autoinstallation needs to be scheduled
+     * @return Returns id of the action if successful, exception otherwise
+     * @throws FaultException A FaultException is thrown if the server corresponding to
+     * id cannot be found or profile is not found.
+     *
+     * @apidoc.doc Provision a system using the specified kickstart/autoinstallation profile.
+     * @apidoc.param #session_key()
+     * @apidoc.param #param_desc("int", "sid", "ID of the system to be provisioned.")
+     * @apidoc.param #param_desc("int", "proxy", "ID of the proxy to use")
+     * @apidoc.param #param_desc("string", "profileName", "Profile to use.")
+     * @apidoc.param #param("$date", "earliestDate")
+     * @apidoc.returntype #param_desc("int", "id", "ID of the action scheduled, otherwise exception thrown
+     * on error")
+     */
+    @ApiIgnore(ApiType.XMLRPC)
+    public int provisionSystem(User loggedInUser, HttpServletRequest request, Integer sid,
+            Integer proxy, String profileName, Date earliestDate)
+        throws FaultException {
         log.debug("provisionSystem called.");
 
         // Lookup the server so we can validate it exists and throw error if not.
@@ -3042,10 +3152,19 @@ public int provisionSystem(User loggedInUser, HttpServletRequest request, Intege
         KickstartHelper helper = new KickstartHelper(request);
         String host = helper.getKickstartHost();
 
-
         KickstartScheduleCommand cmd = new KickstartScheduleCommand(
                 Long.valueOf(sid),
                 ksdata.getId(), loggedInUser, earliestDate, host);
+        if (proxy != null) {
+            Server proxyServer = SystemManager.lookupByIdAndOrg(Long.valueOf(proxy), loggedInUser.getOrg());
+            if (proxyServer == null) {
+                throw new FaultException(-2, "provisionError", "Requested proxy is not available");
+            }
+            if (!proxyServer.isProxy()) {
+                throw new FaultException(-2, "provisionError", "Requested proxy is not registered proxy");
+            }
+            cmd.setProxy(proxyServer);
+        }
         ValidatorError ve = cmd.store();
         if (ve != null) {
             throw new FaultException(-2, "provisionError",
diff --git a/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/test/SystemHandlerProvisioningTest.java b/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/test/SystemHandlerProvisioningTest.java
new file mode 100644
index 00000000000..dae69329d62
--- /dev/null
+++ b/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/test/SystemHandlerProvisioningTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (c) 2024 SUSE LLC
+ *
+ * This software is licensed to you under the GNU General Public License,
+ * version 2 (GPLv2). There is NO WARRANTY for this software, express or
+ * implied, including the implied warranties of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
+ * along with this software; if not, see
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+ *
+ * Red Hat trademarks are not licensed under GPLv2. No permission is
+ * granted to use or replicate Red Hat trademarks that are incorporated
+ * in this software or its documentation.
+ */
+package com.redhat.rhn.frontend.xmlrpc.system.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import com.redhat.rhn.common.conf.ConfigDefaults;
+import com.redhat.rhn.domain.action.Action;
+import com.redhat.rhn.domain.action.ActionFactory;
+import com.redhat.rhn.domain.action.kickstart.KickstartActionDetails;
+import com.redhat.rhn.domain.action.kickstart.KickstartInitiateAction;
+import com.redhat.rhn.domain.kickstart.KickstartData;
+import com.redhat.rhn.domain.kickstart.KickstartFactory;
+import com.redhat.rhn.domain.kickstart.KickstartSession;
+import com.redhat.rhn.domain.kickstart.test.KickstartDataTest;
+import com.redhat.rhn.domain.rhnpackage.Package;
+import com.redhat.rhn.domain.rhnpackage.test.PackageTest;
+import com.redhat.rhn.domain.server.MinionServer;
+import com.redhat.rhn.domain.server.Server;
+import com.redhat.rhn.domain.server.ServerFactory;
+import com.redhat.rhn.domain.server.ServerGroupFactory;
+import com.redhat.rhn.domain.server.test.MinionServerFactoryTest;
+import com.redhat.rhn.frontend.xmlrpc.system.SystemHandler;
+import com.redhat.rhn.frontend.xmlrpc.system.XmlRpcSystemHelper;
+import com.redhat.rhn.frontend.xmlrpc.test.BaseHandlerTestCase;
+import com.redhat.rhn.manager.entitlement.EntitlementManager;
+import com.redhat.rhn.manager.formula.FormulaMonitoringManager;
+import com.redhat.rhn.manager.rhnpackage.test.PackageManagerTest;
+import com.redhat.rhn.manager.system.ServerGroupManager;
+import com.redhat.rhn.manager.system.SystemManager;
+import com.redhat.rhn.manager.system.entitling.SystemEntitlementManager;
+import com.redhat.rhn.manager.system.entitling.SystemEntitler;
+import com.redhat.rhn.manager.system.entitling.SystemUnentitler;
+import com.redhat.rhn.taskomatic.TaskomaticApi;
+import com.redhat.rhn.testing.RhnMockHttpServletRequest;
+import com.redhat.rhn.testing.ServerTestUtils;
+import com.redhat.rhn.testing.TestUtils;
+
+import com.suse.cloud.CloudPaygManager;
+import com.suse.manager.virtualization.VirtManagerSalt;
+import com.suse.manager.webui.controllers.bootstrap.RegularMinionBootstrapper;
+import com.suse.manager.webui.controllers.bootstrap.SSHMinionBootstrapper;
+import com.suse.manager.webui.services.iface.MonitoringManager;
+import com.suse.manager.webui.services.iface.SaltApi;
+import com.suse.manager.webui.services.iface.SystemQuery;
+import com.suse.manager.webui.services.iface.VirtManager;
+import com.suse.manager.webui.services.test.TestSaltApi;
+import com.suse.manager.webui.services.test.TestSystemQuery;
+
+import org.jmock.imposters.ByteBuddyClassImposteriser;
+import org.jmock.junit5.JUnit5Mockery;
+import org.jmock.lib.concurrent.Synchroniser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+@ExtendWith(JUnit5Mockery.class)
+public class SystemHandlerProvisioningTest extends BaseHandlerTestCase {
+
+        private final TaskomaticApi taskomaticApi = new TaskomaticApi();
+        private final SystemQuery systemQuery = new TestSystemQuery();
+        private final SaltApi saltApi = new TestSaltApi();
+        private final CloudPaygManager paygManager = new CloudPaygManager();
+        private RegularMinionBootstrapper regularMinionBootstrapper =
+                new RegularMinionBootstrapper(systemQuery, saltApi, paygManager);
+        private SSHMinionBootstrapper sshMinionBootstrapper = new SSHMinionBootstrapper(systemQuery, saltApi,
+                paygManager);
+        private XmlRpcSystemHelper xmlRpcSystemHelper = new XmlRpcSystemHelper(
+                regularMinionBootstrapper,
+                sshMinionBootstrapper
+        );
+        private final ServerGroupManager serverGroupManager = new ServerGroupManager(saltApi);
+        private final VirtManager virtManager = new VirtManagerSalt(saltApi);
+        private final MonitoringManager monitoringManager = new FormulaMonitoringManager(saltApi);
+        private final SystemEntitlementManager systemEntitlementManager = new SystemEntitlementManager(
+                new SystemUnentitler(virtManager, monitoringManager, serverGroupManager),
+                new SystemEntitler(saltApi, virtManager, monitoringManager, serverGroupManager)
+        );
+        private SystemManager systemManager =
+                new SystemManager(ServerFactory.SINGLETON, ServerGroupFactory.SINGLETON, saltApi);
+        private SystemHandler handler =
+                new SystemHandler(taskomaticApi, xmlRpcSystemHelper, systemEntitlementManager, systemManager,
+                        serverGroupManager, new CloudPaygManager());
+        private HttpServletRequest mockRequest;
+
+        @RegisterExtension
+        protected final JUnit5Mockery mockContext = new JUnit5Mockery() {{
+                setThreadingPolicy(new Synchroniser());
+                setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
+        }};
+
+        @Override
+        @BeforeEach
+        public void setUp() throws Exception {
+                super.setUp();
+                mockRequest = mockContext.mock(HttpServletRequest.class);
+        }
+
+        @Test
+        public void testProvisionSystem() throws Exception {
+                MinionServer server = MinionServerFactoryTest.createTestMinionServer(admin);
+                systemEntitlementManager.setBaseEntitlement(server, EntitlementManager.SALT);
+                TestUtils.saveAndFlush(server);
+                server = reload(server);
+
+                // salt-minion package has to be added to pass Kickstart validator
+                Package testPackage = PackageTest.createTestPackage(admin.getOrg(), "salt-minion");
+                PackageManagerTest.associateSystemToPackage(server, testPackage);
+
+                KickstartDataTest.setupTestConfiguration(admin);
+                KickstartData k = KickstartDataTest.createKickstartWithProfile(admin);
+                KickstartDataTest.addCommand(admin, k,
+                        "custom", "echo test-command");
+                k.getKickstartDefaults().getKstree().setChannel(server.getBaseChannel());
+                String profileName = k.getLabel();
+                RhnMockHttpServletRequest request = new RhnMockHttpServletRequest();
+
+                int result = 0;
+                result = handler.provisionSystem(admin, request, server.getId().intValue(), profileName);
+
+                // something was scheduled
+                assertNotEquals(0, result);
+
+                // KS action is what we scheduled
+                List<KickstartSession> res = KickstartFactory.lookupAllKickstartSessionsByServer(server.getId());
+                assertFalse(res.isEmpty());
+                KickstartSession ks = res.get(0);
+                assertNotNull(ks);
+                assertEquals(server.getId(), ks.getNewServer().getId());
+                assertEquals("echo test-command", ks.getKsdata().getCommand("custom").getArguments());
+
+                // Action details to check correct host
+                List<Action> actions = ActionFactory.listActionsForServer(admin, server);
+                assertNotNull(actions);
+                KickstartInitiateAction kia = (KickstartInitiateAction) actions.get(0);
+                KickstartActionDetails kad = kia.getKickstartActionDetails();
+                assertEquals(ConfigDefaults.get().getHostname(), kad.getKickstartHost());
+
+        }
+
+        @Test
+        public void testProvisionSystemFromProxy() throws Exception {
+                MinionServer server = MinionServerFactoryTest.createTestMinionServer(admin);
+                systemEntitlementManager.setBaseEntitlement(server, EntitlementManager.SALT);
+                TestUtils.saveAndFlush(server);
+                server = reload(server);
+
+                // salt-minion package has to be added to pass Kickstart validator
+                Package testPackage = PackageTest.createTestPackage(admin.getOrg(), "salt-minion");
+                PackageManagerTest.associateSystemToPackage(server, testPackage);
+
+                Server proxy = ServerTestUtils.createTestSystem(admin);
+                systemEntitlementManager.setBaseEntitlement(server, EntitlementManager.SALT);
+                proxy.setHostname("proxy.example.com");
+                SystemManager.activateProxy(proxy, "4.3");
+                TestUtils.saveAndFlush(proxy);
+                proxy = reload(proxy);
+
+                KickstartDataTest.setupTestConfiguration(admin);
+                KickstartData k = KickstartDataTest.createKickstartWithProfile(admin);
+                KickstartDataTest.addCommand(admin, k,
+                        "custom", "echo test-command 2");
+                k.getKickstartDefaults().getKstree().setChannel(server.getBaseChannel());
+                String profileName = k.getLabel();
+                RhnMockHttpServletRequest request = new RhnMockHttpServletRequest();
+
+                int result = 0;
+                result = handler.provisionSystem(admin, request, server.getId().intValue(),
+                        proxy.getId().intValue(), profileName);
+
+                // something was scheduled
+                assertNotEquals(0, result);
+
+                // KS action is what we scheduled
+                List<KickstartSession> res = KickstartFactory.lookupAllKickstartSessionsByServer(server.getId());
+                assertFalse(res.isEmpty());
+                KickstartSession ks = res.get(0);
+                assertNotNull(ks);
+                assertEquals(server.getId(), ks.getNewServer().getId());
+                assertEquals("echo test-command 2", ks.getKsdata().getCommand("custom").getArguments());
+
+                // Action details to check correct host
+                List<Action> actions = ActionFactory.listActionsForServer(admin, server);
+                assertNotNull(actions);
+                KickstartInitiateAction kia = (KickstartInitiateAction) actions.get(0);
+                KickstartActionDetails kad = kia.getKickstartActionDetails();
+                assertEquals(proxy.getHostname(), kad.getKickstartHost());
+        }
+
+        @Test
+        public void testProvisionSystemFromProxyAutoDetected() throws Exception {
+                MinionServer server = MinionServerFactoryTest.createTestMinionServer(admin);
+                systemEntitlementManager.setBaseEntitlement(server, EntitlementManager.SALT);
+                TestUtils.saveAndFlush(server);
+                server = reload(server);
+
+                // salt-minion package has to be added to pass Kickstart validator
+                Package testPackage = PackageTest.createTestPackage(admin.getOrg(), "salt-minion");
+                PackageManagerTest.associateSystemToPackage(server, testPackage);
+
+                Server proxy = ServerTestUtils.createTestSystem(admin);
+                systemEntitlementManager.setBaseEntitlement(server, EntitlementManager.SALT);
+                proxy.setHostname("proxy.example.com");
+                SystemManager.activateProxy(proxy, "4.3");
+                TestUtils.saveAndFlush(proxy);
+                proxy = reload(proxy);
+
+                KickstartDataTest.setupTestConfiguration(admin);
+                KickstartData k = KickstartDataTest.createKickstartWithProfile(admin);
+                KickstartDataTest.addCommand(admin, k,
+                        "custom", "echo test-command 3");
+                k.getKickstartDefaults().getKstree().setChannel(server.getBaseChannel());
+                String profileName = k.getLabel();
+                RhnMockHttpServletRequest request = new RhnMockHttpServletRequest();
+
+                String headerValue = "1006681409::1151513167.96:21600.0:VV/xF" +
+                "NEmCYOuHxEBAs7BEw==:myproxy,1006681408" +
+                "::1151513034.3:21600.0:w2lm+XWSFJMVCGBK1dZXXQ==:fjs-0-11." +
+                "uyunidev.suse.com,1006678487::1152567362.02:21600.0:t15l" +
+                "gsaTRKpX6AxkUFQ11A==:fjs-0-12.rhndev.redhat.com";
+                headerValue = headerValue.replaceFirst("myproxy", proxy.getHostname());
+                request.setHeader("X-RHN-Proxy-Auth", headerValue);
+
+                int result = 0;
+                result = handler.provisionSystem(admin, request, server.getId().intValue(),
+                        profileName);
+
+                // something was scheduled
+                assertNotEquals(0, result);
+
+                // KS action is what we scheduled
+                List<KickstartSession> res = KickstartFactory.lookupAllKickstartSessionsByServer(server.getId());
+                assertFalse(res.isEmpty());
+                KickstartSession ks = res.get(0);
+                assertNotNull(ks);
+                assertEquals(server.getId(), ks.getNewServer().getId());
+                assertEquals("echo test-command 3", ks.getKsdata().getCommand("custom").getArguments());
+
+                // Action details to check correct host
+                List<Action> actions = ActionFactory.listActionsForServer(admin, server);
+                assertNotNull(actions);
+                KickstartInitiateAction kia = (KickstartInitiateAction) actions.get(0);
+                KickstartActionDetails kad = kia.getKickstartActionDetails();
+                assertEquals(proxy.getHostname(), kad.getKickstartHost());
+        }
+}
diff --git a/java/code/src/com/redhat/rhn/testing/RhnMockHttpServletRequest.java b/java/code/src/com/redhat/rhn/testing/RhnMockHttpServletRequest.java
index 4b811e8b430..5309dc614cc 100644
--- a/java/code/src/com/redhat/rhn/testing/RhnMockHttpServletRequest.java
+++ b/java/code/src/com/redhat/rhn/testing/RhnMockHttpServletRequest.java
@@ -1,4 +1,5 @@
 /*
+ * Copyright (c) 2024 SUSE LLC
  * Copyright (c) 2009--2014 Red Hat, Inc.
  *
  * This software is licensed to you under the GNU General Public License,
@@ -112,6 +113,15 @@ public String getHeader(String name) {
         return (String)headers.get(name);
     }
 
+    /**
+     * Add custom header and value
+     * @param name header
+     * @param value header value
+     */
+    public void setHeader(String name, String value) {
+        headers.put(name, value);
+    }
+
     /** {@inheritDoc} */
     public Enumeration getHeaderNames() {
         return this.headerNames;

