From 91805e1d82063b040da47897d1719ea8eb7648ed Mon Sep 17 00:00:00 2001
From: Alexander Olofsson <alexander.olofsson@liu.se>
Date: Mon, 4 Sep 2023 09:53:50 +0200
Subject: [PATCH] Rework testing to properly stub WinRM

---
 app/models/wds_server.rb                  | 83 ++++++++++++-----------
 test/models/foreman_wds/wds_facet_test.rb |  8 ++-
 test/test_plugin_helper.rb                | 45 ++++++++++++
 3 files changed, 94 insertions(+), 42 deletions(-)

diff --git a/app/models/wds_server.rb b/app/models/wds_server.rb
index aaf1cb4..56c360f 100644
--- a/app/models/wds_server.rb
+++ b/app/models/wds_server.rb
@@ -49,12 +49,9 @@ class WdsServer < ApplicationRecord
     objects = run_wql('SELECT * FROM MSFT_WdsClient', on_error: {})[:msft_wdsclient]
     objects = nil if objects&.empty?
     objects ||= begin
-      data = connection.shell(:powershell) do |s|
-        s.run('Get-WdsClient | ConvertTo-Json -Compress')
-      end.stdout
-      data = '[]' if data.empty?
-
-      underscore_result([JSON.parse(data)].flatten)
+      clients = run_pwsh('Get-WdsClient').stdout
+      clients = '[]' if clients.empty?
+      underscore_result([JSON.parse(clients)].flatten)
     end
 
     objects
@@ -82,18 +79,14 @@ class WdsServer < ApplicationRecord
     raise NotImplementedError, 'Not finished yet'
     ensure_unattend(host)
 
-    connection.shell(:powershell) do |sh|
-      sh.run("New-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}' -DeviceName '#{host.name}' -WdsClientUnattend '#{unattend_file(host)}' -BootImagePath 'boot\\#{wdsify_architecture(host.architecture)}\\images\\#{(host.wds_boot_image || boot_images.first).file_name}' -PxePromptPolicy 'NoPrompt'")
-    end
+    run_pwsh("New-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}' -DeviceName '#{host.name}' -WdsClientUnattend '#{unattend_file(host)}' -BootImagePath 'boot\\#{wdsify_architecture(host.architecture)}\\images\\#{(host.wds_boot_image || boot_images.first).file_name}' -PxePromptPolicy 'NoPrompt'")
   end
 
   def delete_client(host)
     raise NotImplementedError, 'Not finished yet'
     delete_unattend(host)
 
-    connection.shell(:powershell) do |sh|
-      sh.run("Remove-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}'")
-    end
+    run_pwsh("Remove-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}'", json: false)
   end
 
   def all_images
@@ -162,9 +155,10 @@ class WdsServer < ApplicationRecord
 
   def unattend_path
     cache.cache(:unattend_path) do
-      JSON.parse(connection.shell(:powershell) do |sh|
-        sh.run('Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\WDSServer\Providers\WDSTFTP -Name RootFolder | select RootFolder | ConvertTo-Json -Compress')
-      end, symbolize_names: true)[:RootFolder]
+      JSON.parse(
+        run_pwsh('Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\WDSServer\Providers\WDSTFTP -Name RootFolder | select RootFolder'),
+        symbolize_names: true
+      )[:RootFolder]
     end
   end
 
@@ -187,6 +181,7 @@ class WdsServer < ApplicationRecord
     raise 'No provisioning interface available' unless iface
 
     raise NotImplementedException, 'TODO: Not implemented yet'
+    raise NotImplementedException, 'TODO: Not implemented yet' if SETTINGS[:wds_unattend_group]
 
     # TODO: render template, send as heredoc
     template = host.operatingsystem.provisioning_templates.find { |t| t.template_kind.name == 'wds_unattend' }
@@ -198,33 +193,33 @@ class WdsServer < ApplicationRecord
 
     template_data = host.render_template template: template
 
-    connection.shell(:powershell) do |sh|
-      file_path = unattend_file(host)
-
-      sh.run("$unattend_render = @'\n#{template_data}\n'@")
-      sh.run("New-Item -Path '#{file_path}' -ItemType 'file' -Value $unattend_render")
-
-      source_image = host.wds_facet.install_image
-      target_image = target_image_for(host)
-      if SETTINGS[:wds_unattend_group]
-        raise NotImplementedException, 'TODO: Not implemented yet'
-        # New-WdsInstallImageGroup -Name #{SETTINGS[:wds_unattend_group]}
-        # Export-WdsInstallImage -ImageGroup <Group> ...
-        # Import-WdsInstallImage -ImageGroup #{SETTINGS[:wds_unattend_group]} -UnattendFile '#{file_path}' -OverwriteUnattend ...
-      else
-        sh.run("Copy-WdsInstallImage -ImageGroup '#{source_image.image_group}' -FileName '#{source_image.file_name}' -ImageName '#{source_image.image_name}' -NewFileName '#{target_image.file_name}' -NewImageName '#{target_image.image_name}'")
-        sh.run("Set-WdsInstallImage -ImageGroup '#{target_image.image_group}' -FileName '#{target_image.file_name}' -ImageName '#{target_image.image_name}' -DisplayOrder 99999 -UnattendFile '#{file_path}' -OverwriteUnattend")
-      end
+    file_path = unattend_file(host)
+    script = []
+    script << "$unattend_render = @'\n#{template_data}\n'@"
+    script << "New-Item -Path '#{file_path}' -ItemType 'file' -Value $unattend_render"
+
+    source_image = host.wds_facet.install_image
+    target_image = target_image_for(host)
+
+    if SETTINGS[:wds_unattend_group]
+      # New-WdsInstallImageGroup -Name #{SETTINGS[:wds_unattend_group]}
+      # Export-WdsInstallImage -ImageGroup <Group> ...
+      # Import-WdsInstallImage -ImageGroup #{SETTINGS[:wds_unattend_group]} -UnattendFile '#{file_path}' -OverwriteUnattend ...
+    else
+      script << "Copy-WdsInstallImage -ImageGroup '#{source_image.image_group}' -FileName '#{source_image.file_name}' -ImageName '#{source_image.image_name}' -NewFileName '#{target_image.file_name}' -NewImageName '#{target_image.image_name}'"
+      script << "Set-WdsInstallImage -ImageGroup '#{target_image.image_group}' -FileName '#{target_image.file_name}' -ImageName '#{target_image.image_name}' -DisplayOrder 99999 -UnattendFile '#{file_path}' -OverwriteUnattend"
     end
+
+    run_pwsh script.join("\n"), json: false
   end
 
   def delete_unattend(host)
     image = target_image_for(host)
 
-    connection.shell(:powershell) do |sh|
-      sh.run("Remove-WdsInstallImage -ImageGroup '#{image.image_group}' -ImageName '#{image.image_name}' -FileName '#{image.file_name}'")
-      sh.run("Remove-Item -Path '#{unattend_file(host)}'")
-    end.errcode.zero?
+    command = []
+    command << "Remove-WdsInstallImage -ImageGroup '#{image.image_group}' -ImageName '#{image.image_name}' -FileName '#{image.file_name}'"
+    command << "Remove-Item -Path '#{unattend_file(host)}'"
+    run_pwsh(command.join("\n"), json: false).errcode.zero?
   end
 
   def ensure_client(_host)
@@ -242,14 +237,12 @@ class WdsServer < ApplicationRecord
     objects = nil if objects.empty?
 
     unless objects
-      begin
-        result = connection.shell(:powershell) do |s|
-          s.run("Get-WDS#{type.to_s.capitalize}Image #{"-ImageName '#{name.sub("'", "`'")}'" if name} | ConvertTo-Json -Compress")
-        end
+      result = run_pwsh "Get-WDS#{type.to_s.capitalize}Image#{" -ImageName '#{name.sub("'", "`'")}'" if name}"
 
+      begin
         objects = underscore_result([JSON.parse(result.stdout)].flatten)
       rescue JSON::ParserError => e
-        ::Rails.logger.error "#{e.class}: #{e}\n#{result}"
+        ::Rails.logger.error "Failed to parse images - #{e.class}: #{e}, the data was;\n#{result.inspect}"
         raise e
       end
     end
@@ -270,6 +263,14 @@ class WdsServer < ApplicationRecord
     end
   end
 
+  def run_pwsh(command, json: true)
+    cmd_arr = [command]
+    cmd_arr << '| ConvertTo-Json -Compress' if json
+    connection.shell(:powershell) do |s|
+      s.run cmd_arr.join(' ')
+    end
+  end
+
   def run_wql(wql, on_error: :raise)
     connection.run_wql(wql)
   rescue StandardError
diff --git a/test/models/foreman_wds/wds_facet_test.rb b/test/models/foreman_wds/wds_facet_test.rb
index 4c8a17c..764dfcc 100644
--- a/test/models/foreman_wds/wds_facet_test.rb
+++ b/test/models/foreman_wds/wds_facet_test.rb
@@ -8,7 +8,7 @@ module ForemanWds
       FactoryBot.build(:wds_server)
     end
     let(:host) do
-      FactoryBot.build(:host, :managed, :with_wds_facet) do
+      FactoryBot.build(:host, :managed, :with_wds_facet) do |host|
         host.wds_facet.wds_server = wds_server
       end
     end
@@ -23,6 +23,12 @@ module ForemanWds
     end
 
     context 'with WDS server' do
+      setup do
+        wds_server.stubs(:run_wql).returns({})
+        wds_server.stubs(:run_pwsh).with('Get-WDSBootImage').returns(OpenStruct.new stdout: '[]')
+        wds_server.stubs(:run_pwsh).with("Get-WDSInstallImage -ImageName 'install.wim'").returns(OpenStruct.new stdout: '[]')
+      end
+
       it 'does not error' do
         assert_nil host.wds_facet.boot_image
         assert_nil host.wds_facet.install_image
diff --git a/test/test_plugin_helper.rb b/test/test_plugin_helper.rb
index a236a6d..28acb8f 100644
--- a/test/test_plugin_helper.rb
+++ b/test/test_plugin_helper.rb
@@ -21,3 +21,48 @@ ActiveSupport::TestCase.file_fixture_path = File.join(__dir__, 'fixtures')
 # Add plugin to FactoryBot's paths
 FactoryBot.definition_file_paths << File.join(__dir__, 'factories')
 FactoryBot.reload
+
+class ActiveSupport::TestCase
+  setup :setup_winrm_stubs
+
+  def stub_winrm_powershell(command = nil, &block)
+    ret = if block_given?
+            @winrm_shell_mock[:powershell].stubs(:run).with(&block)
+          else
+            @winrm_shell_mock[:powershell].stubs(:run).with(command)
+          end
+    class << ret
+      def returns_pwsh(value, **params)
+        returns OpenStruct.new(stdout: value, **params)
+      end
+    end
+    ret
+  end
+
+  def stub_winrm_wql(query = nil)
+    if query
+      WinRM::Connection.any_instance.stubs(:run_wql).with(query)
+    else
+      WinRM::Connection.any_instance.stubs(:run_wql)
+    end
+  end
+
+  private
+
+  def setup_winrm_stubs
+    return if @winrm_mock
+
+    @winrm_mock = true
+    require 'winrm'
+
+    transport_mock = mock('winrm::http::transport')
+    WinRM::Connection.any_instance.stubs(:transport).returns(transport_mock)
+
+    transport_mock.stubs(:send_request).raises(StandardError, 'Real WinRM connections are not allowed')
+
+    @winrm_shell_mock = {
+      powershell: mock('winrm::shell::powershell')
+    }
+    WinRM::Connection.any_instance.stubs(:shell).with(:powershell).yields @winrm_shell_mock[:powershell]
+  end
+end
-- 
GitLab