foreman_openbolt 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +190 -19
  3. data/Rakefile +17 -93
  4. data/app/controllers/foreman_openbolt/task_controller.rb +61 -49
  5. data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +11 -10
  6. data/app/lib/actions/foreman_openbolt/poll_task_status.rb +70 -60
  7. data/app/models/foreman_openbolt/task_job.rb +16 -17
  8. data/config/routes.rb +0 -1
  9. data/lib/foreman_openbolt/engine.rb +11 -11
  10. data/lib/foreman_openbolt/version.rb +1 -1
  11. data/lib/proxy_api/openbolt.rb +25 -9
  12. data/lib/tasks/foreman_openbolt_tasks.rake +1 -22
  13. data/locale/gemspec.rb +1 -1
  14. data/package.json +11 -15
  15. data/test/acceptance/acceptance_helper.rb +146 -0
  16. data/test/acceptance/docker/docker-compose.yml +69 -0
  17. data/test/acceptance/docker/foreman/Dockerfile +45 -0
  18. data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
  19. data/test/acceptance/docker/target/Dockerfile +29 -0
  20. data/test/acceptance/docker/target/entrypoint.sh +11 -0
  21. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
  22. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
  23. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
  24. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
  25. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
  26. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
  27. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
  28. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
  29. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
  30. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
  31. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
  32. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
  33. data/test/acceptance/fixtures/openbolt.yml +7 -0
  34. data/test/acceptance/tests/error_handling_test.rb +40 -0
  35. data/test/acceptance/tests/host_selector_test.rb +31 -0
  36. data/test/acceptance/tests/launch_task_test.rb +96 -0
  37. data/test/acceptance/tests/parameter_table_test.rb +61 -0
  38. data/test/acceptance/tests/settings_test.rb +95 -0
  39. data/test/acceptance/tests/ssh_options_test.rb +77 -0
  40. data/test/acceptance/tests/task_execution_test.rb +40 -0
  41. data/test/acceptance/tests/task_history_test.rb +84 -0
  42. data/test/acceptance/tests/transport_options_test.rb +121 -0
  43. data/test/test_plugin_helper.rb +12 -3
  44. data/test/unit/controllers/task_controller_test.rb +351 -0
  45. data/test/unit/docker/Dockerfile +47 -0
  46. data/test/unit/docker/docker-compose.yml +33 -0
  47. data/test/unit/docker/entrypoint.sh +4 -0
  48. data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
  49. data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
  50. data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
  51. data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
  52. data/test/unit/models/task_job_test.rb +278 -0
  53. data/webpack/__mocks__/foremanReact/common/I18n.js +15 -0
  54. data/webpack/__mocks__/foremanReact/components/ToastsList/index.js +6 -0
  55. data/webpack/__mocks__/foremanReact/redux/API/index.js +11 -0
  56. data/webpack/src/Components/LaunchTask/FieldTable.js +8 -5
  57. data/webpack/src/Components/LaunchTask/HostSelector/SearchSelect.js +74 -62
  58. data/webpack/src/Components/LaunchTask/HostSelector/SelectedChips.js +11 -13
  59. data/webpack/src/Components/LaunchTask/HostSelector/index.js +28 -33
  60. data/webpack/src/Components/LaunchTask/OpenBoltOptionsSection.js +3 -2
  61. data/webpack/src/Components/LaunchTask/ParameterField.js +2 -0
  62. data/webpack/src/Components/LaunchTask/SmartProxySelect.js +2 -1
  63. data/webpack/src/Components/LaunchTask/TaskSelect.js +3 -3
  64. data/webpack/src/Components/LaunchTask/__tests__/EmptyContent.test.js +10 -0
  65. data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +83 -0
  66. data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +86 -0
  67. data/webpack/src/Components/LaunchTask/__tests__/ParametersSection.test.js +50 -0
  68. data/webpack/src/Components/LaunchTask/__tests__/SmartProxySelect.test.js +63 -0
  69. data/webpack/src/Components/LaunchTask/__tests__/TaskSelect.test.js +39 -0
  70. data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +90 -0
  71. data/webpack/src/Components/LaunchTask/hooks/__tests__/useSmartProxies.test.js +69 -0
  72. data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +103 -0
  73. data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +9 -11
  74. data/webpack/src/Components/LaunchTask/hooks/useSmartProxies.js +12 -13
  75. data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +6 -13
  76. data/webpack/src/Components/LaunchTask/index.js +9 -27
  77. data/webpack/src/Components/TaskExecution/ExecutionDetails.js +29 -29
  78. data/webpack/src/Components/TaskExecution/ExecutionDisplay.js +9 -10
  79. data/webpack/src/Components/TaskExecution/LoadingIndicator.js +7 -2
  80. data/webpack/src/Components/TaskExecution/ResultDisplay.js +13 -17
  81. data/webpack/src/Components/TaskExecution/TaskDetails.js +58 -67
  82. data/webpack/src/Components/TaskExecution/__tests__/ExecutionDetails.test.js +47 -0
  83. data/webpack/src/Components/TaskExecution/__tests__/ExecutionDisplay.test.js +29 -0
  84. data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +25 -0
  85. data/webpack/src/Components/TaskExecution/__tests__/ResultDisplay.test.js +28 -0
  86. data/webpack/src/Components/TaskExecution/__tests__/TaskDetails.test.js +38 -0
  87. data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +80 -0
  88. data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +177 -0
  89. data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +34 -33
  90. data/webpack/src/Components/TaskExecution/index.js +10 -12
  91. data/webpack/src/Components/TaskHistory/TaskPopover.js +9 -12
  92. data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +109 -0
  93. data/webpack/src/Components/TaskHistory/__tests__/TaskPopover.test.js +26 -0
  94. data/webpack/src/Components/TaskHistory/index.js +21 -29
  95. data/webpack/src/Components/common/HostsPopover.js +12 -3
  96. data/webpack/src/Components/common/__tests__/HostsPopover.test.js +20 -0
  97. data/webpack/src/Components/common/__tests__/helpers.test.js +135 -0
  98. data/webpack/src/Components/common/helpers.js +34 -5
  99. data/webpack/test_setup.js +34 -11
  100. metadata +65 -87
  101. data/test/factories/foreman_openbolt_factories.rb +0 -7
  102. data/test/unit/foreman_openbolt_test.rb +0 -13
  103. data/webpack/global_test_setup.js +0 -11
  104. data/webpack/webpack.config.js +0 -7
@@ -0,0 +1,69 @@
1
+ name: foreman-openbolt-acceptance
2
+
3
+ services:
4
+ foreman:
5
+ container_name: foreman-openbolt-test
6
+ hostname: foreman.example.com
7
+ image: ${FOREMAN_IMAGE:-foreman-openbolt:3.18}
8
+ pull_policy: never
9
+ platform: linux/amd64
10
+ privileged: true
11
+ ports:
12
+ - "443:443"
13
+ - "8443:8443"
14
+ entrypoint: /entrypoint.sh
15
+ volumes:
16
+ # Entrypoint script (prepares SSH keys, fixtures, then starts systemd)
17
+ - ./foreman/entrypoint.sh:/entrypoint.sh:ro
18
+ # Plugin RPMs built by rake build:rpm
19
+ - ../../../pkg:/opt/pkg:ro
20
+ # Fixture modules for acceptance tasks
21
+ - ../fixtures/modules:/opt/fixtures/modules:ro
22
+ # SSH private key for bolt to connect to targets
23
+ - ../fixtures/keys/id_rsa:/tmp/ssh/id_rsa:ro
24
+ # OpenBolt plugin config for smart-proxy
25
+ - ../fixtures/openbolt.yml:/etc/foreman-proxy/settings.d/openbolt.yml:ro
26
+ depends_on:
27
+ - target1
28
+ - target2
29
+ tmpfs:
30
+ - /run
31
+ - /tmp:rw,exec,nosuid
32
+ healthcheck:
33
+ test: ["CMD", "curl", "-sk", "https://localhost/api/v2/status"]
34
+ interval: 10s
35
+ timeout: 10s
36
+ retries: 30
37
+ start_period: 60s
38
+
39
+ chrome:
40
+ container_name: foreman-openbolt-chrome
41
+ image: ${SELENIUM_IMAGE:-selenium/standalone-chrome:latest}
42
+ shm_size: 2g
43
+ ports:
44
+ - "4444:4444"
45
+ - "7900:7900"
46
+ depends_on:
47
+ - foreman
48
+
49
+ target1:
50
+ container_name: foreman-openbolt-target1
51
+ hostname: target1.example.com
52
+ image: foreman-openbolt-target
53
+ pull_policy: never
54
+ build:
55
+ context: .
56
+ dockerfile: target/Dockerfile
57
+ volumes:
58
+ - ../fixtures/keys/id_rsa.pub:/tmp/id_rsa.pub:ro
59
+
60
+ target2:
61
+ container_name: foreman-openbolt-target2
62
+ hostname: target2.example.com
63
+ image: foreman-openbolt-target
64
+ pull_policy: never
65
+ build:
66
+ context: .
67
+ dockerfile: target/Dockerfile
68
+ volumes:
69
+ - ../fixtures/keys/id_rsa.pub:/tmp/id_rsa.pub:ro
@@ -0,0 +1,45 @@
1
+ # Foreman base image for acceptance testing.
2
+ # This Dockerfile only installs packages. foreman-installer is run
3
+ # separately via `docker run --hostname` (see Rakefile) because it
4
+ # requires a valid FQDN and systemd, neither of which are available
5
+ # during `docker build`.
6
+ #
7
+ # The final image is created by committing the container after
8
+ # foreman-installer completes. Plugin RPMs are installed at runtime
9
+ # via setup.sh.
10
+ FROM --platform=linux/amd64 rockylinux:9
11
+
12
+ ARG FOREMAN_VERSION=3.18
13
+
14
+ # Locale
15
+ RUN dnf install -y glibc-langpack-en && dnf clean all
16
+ ENV LANG=en_US.UTF-8
17
+ ENV PATH="$PATH:/opt/puppetlabs/bin:/opt/puppetlabs/server/bin"
18
+
19
+ # Enable module streams
20
+ RUN dnf -y module enable nodejs:22 postgresql:16
21
+
22
+ # Add Foreman and OpenVox repos
23
+ RUN dnf install -y \
24
+ "https://yum.theforeman.org/releases/${FOREMAN_VERSION}/el9/x86_64/foreman-release.rpm" \
25
+ https://yum.voxpupuli.org/openvox8-release-el-9.noarch.rpm && \
26
+ dnf clean all
27
+
28
+ # Install foreman-installer, OpenBolt, and OpenVox (agent + server)
29
+ RUN dnf install -y \
30
+ foreman-installer \
31
+ jq \
32
+ openbolt \
33
+ openssh-clients \
34
+ openvox-agent \
35
+ openvox-server && \
36
+ dnf clean all
37
+
38
+ # Fix NSS for container environment. Rocky 9 defaults to "sss files systemd"
39
+ # but sssd isn't running, causing getpwuid(0) to fail in JRuby/puppetserver.
40
+ RUN sed -i 's/^passwd:.*/passwd: files/' /etc/nsswitch.conf && \
41
+ sed -i 's/^shadow:.*/shadow: files/' /etc/nsswitch.conf && \
42
+ sed -i 's/^group:.*/group: files/' /etc/nsswitch.conf
43
+
44
+
45
+ CMD ["/usr/sbin/init"]
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+ # Prepare the Foreman container for acceptance testing.
3
+ # This runs before systemd (PID 1) takes over.
4
+ set -e
5
+
6
+ # Proxy log directory
7
+ mkdir -p /var/log/foreman-proxy/openbolt
8
+ chown -R foreman-proxy:foreman-proxy /var/log/foreman-proxy
9
+
10
+ # SSH key for proxy to reach targets
11
+ if [ -f /tmp/ssh/id_rsa ]; then
12
+ mkdir -p /opt/foreman-proxy/.ssh
13
+ chown foreman-proxy:foreman-proxy /opt/foreman-proxy /opt/foreman-proxy/.ssh
14
+ cp /tmp/ssh/id_rsa /opt/foreman-proxy/.ssh/id_rsa
15
+ chown foreman-proxy:foreman-proxy /opt/foreman-proxy/.ssh/id_rsa
16
+ chmod 600 /opt/foreman-proxy/.ssh/id_rsa
17
+ fi
18
+
19
+ # Deploy fixture modules for acceptance tasks
20
+ if [ -d /opt/fixtures/modules ]; then
21
+ mkdir -p /etc/puppetlabs/code/environments/production/modules
22
+ cp -r /opt/fixtures/modules/* /etc/puppetlabs/code/environments/production/modules/
23
+ fi
24
+
25
+ # Hand off to systemd
26
+ exec /usr/sbin/init
@@ -0,0 +1,29 @@
1
+ FROM rockylinux:9
2
+
3
+ ARG OPENVOX_RELEASE=8
4
+
5
+ RUN dnf install -y \
6
+ openssh-server \
7
+ openssh-clients \
8
+ sudo \
9
+ jq \
10
+ https://yum.voxpupuli.org/openvox${OPENVOX_RELEASE}-release-el-9.noarch.rpm && \
11
+ dnf install -y openvox-agent && \
12
+ dnf clean all
13
+
14
+ RUN useradd -m openbolt && \
15
+ echo 'openbolt:openbolt' | chpasswd && \
16
+ usermod -aG wheel openbolt && \
17
+ echo 'openbolt ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/openbolt
18
+
19
+ RUN mkdir -p /home/openbolt/.ssh && \
20
+ chmod 700 /home/openbolt/.ssh && \
21
+ chown -R openbolt:openbolt /home/openbolt
22
+
23
+ RUN ssh-keygen -A
24
+
25
+ COPY target/entrypoint.sh /entrypoint.sh
26
+ RUN chmod +x /entrypoint.sh
27
+
28
+ EXPOSE 22
29
+ ENTRYPOINT ["/entrypoint.sh"]
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # Copy the bind-mounted public key into the openbolt user's authorized_keys
3
+ # with correct ownership and permissions. sshd refuses keys when the
4
+ # file or parent directory is writable by anyone other than the owner.
5
+ set -e
6
+
7
+ cp /tmp/id_rsa.pub /home/openbolt/.ssh/authorized_keys
8
+ chown openbolt:openbolt /home/openbolt/.ssh/authorized_keys
9
+ chmod 600 /home/openbolt/.ssh/authorized_keys
10
+
11
+ exec /usr/sbin/sshd -D
@@ -0,0 +1,30 @@
1
+ {
2
+ "input_method": "environment",
3
+ "description": "A task with multiple parameter types for testing command building",
4
+ "parameters": {
5
+ "required_string": {
6
+ "type": "String",
7
+ "description": "A required string parameter"
8
+ },
9
+ "optional_string": {
10
+ "type": "Optional[String]",
11
+ "description": "An optional string parameter"
12
+ },
13
+ "array_param": {
14
+ "type": "Array",
15
+ "description": "An array parameter"
16
+ },
17
+ "with_default": {
18
+ "type": "String",
19
+ "description": "A parameter with a default value",
20
+ "default": "default_value"
21
+ },
22
+ "hash_param": {
23
+ "type": "Optional[Hash]",
24
+ "description": "An optional hash parameter"
25
+ }
26
+ },
27
+ "implementations": [
28
+ {"name": "complex_params.sh", "requirements": ["shell"]}
29
+ ]
30
+ }
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # Echo back all parameters as JSON so the test can verify they arrived correctly.
3
+ # PT_array_param arrives as a JSON string from bolt, so we pass it through as raw JSON.
4
+ jq -n \
5
+ --arg required "$PT_required_string" \
6
+ --arg optional "$PT_optional_string" \
7
+ --argjson array "${PT_array_param:-null}" \
8
+ --arg default_val "$PT_with_default" \
9
+ --argjson hash "${PT_hash_param:-null}" \
10
+ '{
11
+ required_string: $required,
12
+ optional_string: (if $optional == "" then null else $optional end),
13
+ array_param: $array,
14
+ with_default: $default_val,
15
+ hash_param: $hash
16
+ }'
@@ -0,0 +1,13 @@
1
+ {
2
+ "input_method": "environment",
3
+ "description": "Echo a message back with the hostname",
4
+ "parameters": {
5
+ "message": {
6
+ "type": "String",
7
+ "description": "The message to echo back"
8
+ }
9
+ },
10
+ "implementations": [
11
+ {"name": "echo.sh", "requirements": ["shell"]}
12
+ ]
13
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ jq -n --arg msg "$PT_message" --arg host "$(hostname)" \
3
+ '{"message": $msg, "hostname": $host}'
@@ -0,0 +1,8 @@
1
+ {
2
+ "input_method": "environment",
3
+ "description": "A task that always fails",
4
+ "parameters": {},
5
+ "implementations": [
6
+ {"name": "failing_task.sh", "requirements": ["shell"]}
7
+ ]
8
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ echo '{"status": "failure", "error": "This task always fails"}' >&2
3
+ exit 1
@@ -0,0 +1,8 @@
1
+ {
2
+ "input_method": "environment",
3
+ "description": "A no-op task that returns a status",
4
+ "parameters": {},
5
+ "implementations": [
6
+ {"name": "noop_task.sh", "requirements": ["shell"]}
7
+ ]
8
+ }
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ echo '{"status": "ok"}'
@@ -0,0 +1,14 @@
1
+ {
2
+ "input_method": "environment",
3
+ "description": "A task that sleeps for a specified number of seconds",
4
+ "parameters": {
5
+ "seconds": {
6
+ "type": "Integer",
7
+ "description": "Number of seconds to sleep",
8
+ "default": 5
9
+ }
10
+ },
11
+ "implementations": [
12
+ {"name": "slow_task.sh", "requirements": ["shell"]}
13
+ ]
14
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ sleep "${PT_seconds:-5}"
3
+ echo "{\"status\": \"ok\", \"slept\": ${PT_seconds:-5}}"
@@ -0,0 +1,13 @@
1
+ {
2
+ "input_method": "environment",
3
+ "description": "A task that succeeds on target1 and fails on target2",
4
+ "parameters": {
5
+ "succeed_on": {
6
+ "type": "String",
7
+ "description": "Hostname prefix that should succeed"
8
+ }
9
+ },
10
+ "implementations": [
11
+ {"name": "target_conditional.sh", "requirements": ["shell"]}
12
+ ]
13
+ }
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ hostname=$(hostname)
3
+ if [[ "$hostname" == ${PT_succeed_on}* ]]; then
4
+ echo "{\"status\": \"success\", \"hostname\": \"$hostname\"}"
5
+ exit 0
6
+ else
7
+ echo "{\"status\": \"failure\", \"hostname\": \"$hostname\"}" >&2
8
+ exit 1
9
+ fi
@@ -0,0 +1,7 @@
1
+ ---
2
+ :enabled: https
3
+ :environment_path: /etc/puppetlabs/code/environments/production
4
+ :workers: 5
5
+ :concurrency: 20
6
+ :connect_timeout: 30
7
+ :log_dir: /var/log/foreman-proxy/openbolt
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../acceptance_helper'
4
+
5
+ # Tests that errors are handled gracefully through the full stack.
6
+ class ErrorHandlingTest < AcceptanceTestCase
7
+ def setup
8
+ super
9
+ foreman_login
10
+ end
11
+
12
+ def test_launch_button_disabled_with_no_matching_hosts
13
+ visit '/foreman_openbolt/page_launch_task'
14
+ assert_selector '#smart-proxy-input', wait: 15
15
+ select_first_proxy
16
+ select_hosts_via_search('nonexistent.example.com')
17
+
18
+ assert_selector '#task-name-input option', minimum: 2, wait: 15
19
+ select 'acceptance::echo', from: 'task-name-input'
20
+
21
+ assert_selector 'button[type="submit"][disabled]', text: 'Launch Task'
22
+ end
23
+
24
+ def test_failing_task_shows_error_in_result
25
+ launch_task_via_ui('acceptance::failing_task')
26
+ assert_task_failed
27
+ assert_result_contains 'This task always fails'
28
+ end
29
+
30
+ def test_mixed_target_results_show_per_host_status
31
+ launch_task_via_ui('acceptance::target_conditional',
32
+ params: { 'succeed_on' => 'target1' })
33
+
34
+ assert_task_failed
35
+ assert_result_has_content
36
+ # Result should contain output from both targets
37
+ assert_result_contains 'target1'
38
+ assert_result_contains 'target2'
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../acceptance_helper'
4
+
5
+ # Tests for HostSelector-specific affordances: the search query chip
6
+ # and the "Clear all target selections" link.
7
+ class HostSelectorTest < AcceptanceTestCase
8
+ def setup
9
+ super
10
+ foreman_login
11
+ visit '/foreman_openbolt/page_launch_task'
12
+ assert_selector '#smart-proxy-input', wait: 15
13
+ select_first_proxy
14
+ end
15
+
16
+ def test_clear_chips_link_empties_targets_and_removes_chip
17
+ select_hosts_via_search('target1')
18
+
19
+ # The Targets label is conditional on targets.length > 0
20
+ # (LaunchTask/index.js:230).
21
+ assert_selector '.pf-v5-c-label', text: /Targets:\s*\d+/, wait: 15
22
+
23
+ # Click the "Clear all target selections" button (ouiaId=clear-chips).
24
+ find('[data-ouia-component-id="clear-chips"]').click
25
+
26
+ # Targets label disappears and the clear-chips button itself
27
+ # unmounts once there are no selections.
28
+ assert_no_selector '.pf-v5-c-label', text: /Targets:/, wait: 10
29
+ assert_no_selector '[data-ouia-component-id="clear-chips"]', wait: 5
30
+ end
31
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../acceptance_helper'
4
+
5
+ # Tests launching various task types and verifying their execution results.
6
+ class LaunchTaskTest < AcceptanceTestCase
7
+ def setup
8
+ super
9
+ foreman_login
10
+ end
11
+
12
+ def test_echo_task_succeeds_on_all_targets
13
+ launch_task_via_ui('acceptance::echo',
14
+ params: { 'message' => 'hello from acceptance test' })
15
+
16
+ assert_task_completed
17
+ assert_result_has_content
18
+ assert_result_contains 'hello from acceptance test'
19
+ assert_result_contains 'hostname'
20
+ end
21
+
22
+ def test_noop_task_succeeds
23
+ launch_task_via_ui('acceptance::noop_task')
24
+ assert_task_completed
25
+ assert_result_has_content
26
+ end
27
+
28
+ def test_complex_params_task_succeeds
29
+ launch_task_via_ui('acceptance::complex_params',
30
+ params: {
31
+ 'required_string' => 'test_value',
32
+ 'array_param' => '["a","b","c"]',
33
+ 'with_default' => 'overridden',
34
+ })
35
+
36
+ assert_task_completed
37
+ assert_result_has_content
38
+ assert_result_contains 'test_value'
39
+ assert_result_contains 'overridden'
40
+ end
41
+
42
+ def test_slow_task_transitions_through_running
43
+ launch_task_via_ui('acceptance::slow_task', params: { 'seconds' => '8' })
44
+ assert_selector '.pf-v5-c-label', text: /Running/i, wait: 30
45
+ assert_task_completed
46
+ assert_result_has_content
47
+ end
48
+
49
+ def test_failing_task_shows_failure_with_error_detail
50
+ launch_task_via_ui('acceptance::failing_task')
51
+ assert_task_failed
52
+ assert_result_contains 'This task always fails'
53
+ end
54
+
55
+ def test_run_another_task_navigates_back
56
+ launch_task_via_ui('acceptance::noop_task')
57
+ assert_task_completed
58
+ click_button 'Run Another Task'
59
+ assert_selector 'h1', text: 'Launch OpenBolt Task', wait: 15
60
+ end
61
+
62
+ def test_launch_button_disabled_until_all_selections_made
63
+ visit '/foreman_openbolt/page_launch_task'
64
+ assert_selector '#smart-proxy-input', wait: 15
65
+
66
+ # No selections at all
67
+ assert find('button', text: /Launch Task/).disabled?,
68
+ 'Expected Launch Task disabled with no selections'
69
+
70
+ # Proxy only
71
+ select_first_proxy
72
+ assert_selector '#task-name-input option', minimum: 2, wait: 15
73
+ assert find('button', text: /Launch Task/).disabled?,
74
+ 'Expected Launch Task disabled with only proxy selected'
75
+
76
+ # Proxy + task, no targets
77
+ select 'acceptance::noop_task', from: 'task-name-input'
78
+ assert find('button', text: /Launch Task/).disabled?,
79
+ 'Expected Launch Task disabled with no targets'
80
+
81
+ # Proxy + task + targets — button enables
82
+ select_hosts_via_search('target1')
83
+ assert_selector 'button:not([disabled])', text: /Launch Task/, wait: 10
84
+ end
85
+
86
+ def test_running_task_shows_loading_indicator
87
+ launch_task_via_ui('acceptance::slow_task', params: { 'seconds' => '8' })
88
+ # While the job is still polling, LoadingIndicator renders an EmptyState
89
+ # with role=status and a title of "Task is <status>..." (running or pending).
90
+ assert_selector '[role="status"]',
91
+ text: /Task is (running|pending)/i, wait: 30
92
+ assert_selector '.pf-v5-c-empty-state__body',
93
+ text: /update automatically when the task completes/, wait: 5
94
+ assert_task_completed
95
+ end
96
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../acceptance_helper'
4
+
5
+ # Tests the parameter table rendering logic: required indicator
6
+ # based on the Optional[...] type prefix, and the expandable row
7
+ # that reveals type and description for each parameter.
8
+ class ParameterTableTest < AcceptanceTestCase
9
+ def setup
10
+ super
11
+ foreman_login
12
+ visit '/foreman_openbolt/page_launch_task'
13
+ assert_selector '#smart-proxy-input', wait: 15
14
+ select_first_proxy
15
+ assert_selector '#task-name-input option', minimum: 2, wait: 15
16
+ select 'acceptance::complex_params', from: 'task-name-input'
17
+ assert_selector '#param_required_string', wait: 10
18
+ end
19
+
20
+ def test_required_param_shows_required_indicator
21
+ # ParametersSection flags params whose type does not start with
22
+ # "optional" as required, and FieldTable renders a span with
23
+ # role=img and aria-label="Required" in those rows.
24
+ within(:xpath, "//tr[.//input[@id='param_required_string']]") do
25
+ assert_selector 'span[role="img"][aria-label="Required"]'
26
+ end
27
+ end
28
+
29
+ def test_optional_param_has_no_required_indicator
30
+ within(:xpath, "//tr[.//input[@id='param_optional_string']]") do
31
+ assert_no_selector 'span[role="img"][aria-label="Required"]'
32
+ end
33
+ end
34
+
35
+ def test_expanding_required_param_row_shows_type_description_and_required_warning
36
+ within(:xpath, "//tr[.//input[@id='param_required_string']]") do
37
+ first('button').click
38
+ end
39
+
40
+ # The expanded row is a sibling in the same Tbody. Assertions on
41
+ # the page level since the ExpandableRowContent is outside the
42
+ # trigger row's scope.
43
+ assert_selector '.pf-v5-c-helper-text__item',
44
+ text: 'This field is required', wait: 5
45
+ assert_selector '.pf-v5-c-helper-text__item code', text: 'String'
46
+ assert_selector '.pf-v5-c-helper-text__item',
47
+ text: 'A required string parameter'
48
+ end
49
+
50
+ def test_expanding_optional_param_row_shows_type_and_description_without_required_warning
51
+ within(:xpath, "//tr[.//input[@id='param_optional_string']]") do
52
+ first('button').click
53
+ end
54
+
55
+ assert_selector '.pf-v5-c-helper-text__item code', text: 'Optional[String]'
56
+ assert_selector '.pf-v5-c-helper-text__item',
57
+ text: 'An optional string parameter'
58
+ assert_no_selector '.pf-v5-c-helper-text__item',
59
+ text: 'This field is required'
60
+ end
61
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../acceptance_helper'
4
+
5
+ # Tests that Foreman settings correctly populate the OpenBolt options UI
6
+ # and that changes to settings are reflected in the plugin's behavior.
7
+ # This exercises the settings lookup bug fix (Foreman.settings vs Setting.where).
8
+ class SettingsTest < AcceptanceTestCase
9
+ def setup
10
+ super
11
+ foreman_login
12
+ end
13
+
14
+ def test_openbolt_user_setting_populates_default
15
+ visit '/foreman_openbolt/page_launch_task'
16
+ assert_selector '#smart-proxy-input', wait: 15
17
+ select_first_proxy
18
+
19
+ # The user option should show the value configured during acceptance:up
20
+ assert_selector '#param_user', wait: 10
21
+ assert_equal 'openbolt', find_by_id('param_user').value
22
+ end
23
+
24
+ # The string the UI shows in an encrypted-default field so the user can tell
25
+ # a saved value exists without exposing it to the browser. Kept in sync with
26
+ # ENCRYPTED_PLACEHOLDER in task_controller.rb and ENCRYPTED_DEFAULT_PLACEHOLDER
27
+ # in webpack/src/Components/common/constants.js.
28
+ ENCRYPTED_DEFAULT_PLACEHOLDER = '[Use saved encrypted default]'
29
+
30
+ def test_encrypted_setting_shows_placeholder_and_is_overrideable
31
+ # Scope the saved password to this test so it does not leak into other
32
+ # task launches (merge_encrypted_defaults would inject it into bolt).
33
+ update_foreman_setting('openbolt_password', 'acceptance-test-password')
34
+ begin
35
+ visit '/foreman_openbolt/page_launch_task'
36
+ assert_selector '#smart-proxy-input', wait: 15
37
+ select_first_proxy
38
+
39
+ # With a saved encrypted setting, the field prefills with the placeholder
40
+ # string so the user knows the saved value will be used. The real value
41
+ # is never sent to the browser.
42
+ field = find_by_id('param_password', wait: 10)
43
+ assert_equal 'password', field['type']
44
+ assert_equal ENCRYPTED_DEFAULT_PLACEHOLDER, field.value.to_s
45
+
46
+ # Expanding the row reveals a warning that an encrypted default is saved.
47
+ # The FieldTable renders an expand toggle in the first cell of each row;
48
+ # click the first button within the row containing our field.
49
+ within(:xpath, "//tr[.//input[@id='param_password']]") do
50
+ first('button').click
51
+ end
52
+ assert_selector '.pf-v5-c-helper-text__item',
53
+ text: /saved, encrypted default/i, wait: 10
54
+
55
+ # Typing a new value must replace the placeholder so the task uses
56
+ # what the user entered instead of the saved default.
57
+ field.fill_in with: 'override-value'
58
+ assert_equal 'override-value', field.value.to_s
59
+ ensure
60
+ update_foreman_setting('openbolt_password', '')
61
+ end
62
+ end
63
+
64
+ def test_changing_user_setting_updates_launch_page
65
+ # Change the setting through the Foreman settings UI
66
+ update_foreman_setting('openbolt_user', 'newuser')
67
+
68
+ # Verify the launch page reflects the change
69
+ visit '/foreman_openbolt/page_launch_task'
70
+ assert_selector '#smart-proxy-input', wait: 15
71
+ select_first_proxy
72
+ assert_selector '#param_user', wait: 10
73
+ assert_equal 'newuser', find_by_id('param_user').value
74
+
75
+ # Restore the original value
76
+ update_foreman_setting('openbolt_user', 'openbolt')
77
+ end
78
+
79
+ def test_host_key_check_setting_flows_through_to_checkbox_state
80
+ visit '/foreman_openbolt/page_launch_task'
81
+ assert_selector '#smart-proxy-input', wait: 15
82
+ select_first_proxy
83
+
84
+ # openbolt_host-key-check is a boolean Foreman setting; the
85
+ # acceptance fixture (rakelib/acceptance.rake) sets it to false so
86
+ # SSH does not reject the ephemeral target containers. The registered
87
+ # default in engine.rb is true, so if the Foreman setting lookup
88
+ # were broken, the checkbox would render checked. Asserting
89
+ # unchecked here proves the configured value (false) flowed through.
90
+ field = find_by_id('param_host-key-check', wait: 10)
91
+ assert_equal 'checkbox', field['type']
92
+ refute field.checked?,
93
+ 'Expected host-key-check to be unchecked, matching the acceptance fixture value'
94
+ end
95
+ end