foreman_openbolt 1.1.0 → 1.2.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -252
  3. data/Rakefile +0 -0
  4. data/app/controllers/foreman_openbolt/task_controller.rb +4 -1
  5. data/lib/foreman_openbolt/engine.rb +128 -24
  6. data/lib/foreman_openbolt/version.rb +1 -1
  7. data/lib/proxy_api/openbolt.rb +13 -3
  8. data/package.json +1 -1
  9. data/test/acceptance/acceptance_helper.rb +146 -0
  10. data/test/acceptance/docker/docker-compose.yml +69 -0
  11. data/test/acceptance/docker/foreman/Dockerfile +45 -0
  12. data/test/acceptance/docker/foreman/entrypoint.sh +26 -0
  13. data/test/acceptance/docker/target/Dockerfile +29 -0
  14. data/test/acceptance/docker/target/entrypoint.sh +11 -0
  15. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.json +30 -0
  16. data/test/acceptance/fixtures/modules/acceptance/tasks/complex_params.sh +16 -0
  17. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.json +13 -0
  18. data/test/acceptance/fixtures/modules/acceptance/tasks/echo.sh +3 -0
  19. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.json +8 -0
  20. data/test/acceptance/fixtures/modules/acceptance/tasks/failing_task.sh +3 -0
  21. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.json +8 -0
  22. data/test/acceptance/fixtures/modules/acceptance/tasks/noop_task.sh +2 -0
  23. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.json +14 -0
  24. data/test/acceptance/fixtures/modules/acceptance/tasks/slow_task.sh +3 -0
  25. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.json +13 -0
  26. data/test/acceptance/fixtures/modules/acceptance/tasks/target_conditional.sh +9 -0
  27. data/test/acceptance/fixtures/openbolt.yml +7 -0
  28. data/test/acceptance/tests/error_handling_test.rb +40 -0
  29. data/test/acceptance/tests/host_selector_test.rb +31 -0
  30. data/test/acceptance/tests/launch_task_test.rb +96 -0
  31. data/test/acceptance/tests/parameter_table_test.rb +61 -0
  32. data/test/acceptance/tests/settings_test.rb +95 -0
  33. data/test/acceptance/tests/ssh_options_test.rb +77 -0
  34. data/test/acceptance/tests/task_execution_test.rb +40 -0
  35. data/test/acceptance/tests/task_history_test.rb +84 -0
  36. data/test/acceptance/tests/transport_options_test.rb +161 -0
  37. data/test/test_plugin_helper.rb +17 -0
  38. data/test/unit/controllers/task_controller_test.rb +426 -0
  39. data/test/unit/docker/Dockerfile +47 -0
  40. data/test/unit/docker/docker-compose.yml +33 -0
  41. data/test/unit/docker/entrypoint.sh +4 -0
  42. data/test/unit/factories/foreman_openbolt_factories.rb +39 -0
  43. data/test/unit/lib/actions/cleanup_proxy_artifacts_test.rb +51 -0
  44. data/test/unit/lib/actions/poll_task_status_test.rb +141 -0
  45. data/test/unit/lib/proxy_api/openbolt_test.rb +174 -0
  46. data/test/unit/models/task_job_test.rb +278 -0
  47. data/webpack/src/Components/LaunchTask/__tests__/ParameterField.test.js +45 -0
  48. data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +1 -0
  49. data/webpack/src/Components/TaskExecution/__tests__/LoadingIndicator.test.js +1 -1
  50. data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +1 -1
  51. metadata +39 -1
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara'
4
+ require 'capybara/dsl'
5
+ require 'selenium-webdriver'
6
+ require 'test/unit'
7
+
8
+ # Base class for acceptance tests using Capybara + Selenium Chrome.
9
+ # Connects to Foreman through a remote ChromeDriver container and
10
+ # exercises the plugin UI as a real user would.
11
+ class AcceptanceTestCase < Test::Unit::TestCase
12
+ include Capybara::DSL
13
+
14
+ # Chrome runs in a separate container and reaches Foreman via the Docker
15
+ # network service name. The test runner connects to ChromeDriver via the
16
+ # exposed port 4444 on the host.
17
+ FOREMAN_URL = ENV.fetch('FOREMAN_URL', 'https://foreman')
18
+ FOREMAN_USER = ENV.fetch('FOREMAN_USER', 'admin')
19
+ FOREMAN_PASS = ENV.fetch('FOREMAN_PASS', 'changeme')
20
+ CHROMEDRIVER_URL = ENV.fetch('CHROMEDRIVER_URL', 'http://localhost:4444')
21
+
22
+ def setup
23
+ Capybara.app_host = FOREMAN_URL
24
+ Capybara.run_server = false
25
+ Capybara.default_max_wait_time = 15
26
+
27
+ Capybara.register_driver :remote_chrome do |app|
28
+ options = Selenium::WebDriver::Chrome::Options.new
29
+ options.add_argument('--headless') unless ENV['HEADFUL']
30
+ options.add_argument('--no-sandbox')
31
+ options.add_argument('--disable-dev-shm-usage')
32
+ options.add_argument('--disable-gpu')
33
+ options.add_argument('--window-size=1280,720')
34
+ options.add_argument('--ignore-certificate-errors')
35
+
36
+ Capybara::Selenium::Driver.new(
37
+ app,
38
+ browser: :remote,
39
+ url: CHROMEDRIVER_URL,
40
+ options: options
41
+ )
42
+ end
43
+
44
+ Capybara.default_driver = :remote_chrome
45
+ Capybara.javascript_driver = :remote_chrome
46
+ end
47
+
48
+ def teardown
49
+ visit '/users/logout'
50
+ Capybara.reset_sessions!
51
+ end
52
+
53
+ def foreman_login(user: FOREMAN_USER, password: FOREMAN_PASS)
54
+ visit '/users/login'
55
+ fill_in 'login_login', with: user
56
+ fill_in 'login_password', with: password
57
+ click_button 'Log In'
58
+ end
59
+
60
+ # --- Launch page helpers ---
61
+
62
+ def select_first_proxy
63
+ assert_selector '#smart-proxy-input option', minimum: 2, wait: 15
64
+ proxy_option = first('#smart-proxy-input option:not([value=""])')
65
+ select proxy_option.text, from: 'smart-proxy-input'
66
+ end
67
+
68
+ def select_hosts_via_search(query)
69
+ find('[aria-label="Select host targeting method"]').click
70
+ find('[data-ouia-component-id="host_methods"]').find('li', text: 'Search query').click
71
+ search_input = find('.foreman-search-field input[type="text"]', wait: 10)
72
+ search_input.fill_in with: query
73
+ end
74
+
75
+ def launch_task_via_ui(task_name, targets: 'target', params: {})
76
+ visit '/foreman_openbolt/page_launch_task'
77
+ assert_selector '#smart-proxy-input', wait: 15
78
+
79
+ select_first_proxy
80
+ select_hosts_via_search(targets)
81
+
82
+ assert_selector '#task-name-input option', minimum: 2, wait: 15
83
+ select task_name, from: 'task-name-input'
84
+
85
+ params.each do |name, value|
86
+ assert_selector "#param_#{name}", wait: 10
87
+ fill_in "param_#{name}", with: value
88
+ end
89
+
90
+ click_button 'Launch Task'
91
+ assert_selector 'h1', text: 'Task Execution', wait: 15
92
+ end
93
+
94
+ def assert_task_completed
95
+ assert_selector '.pf-v5-c-label', text: /Success|Complete/i, wait: 120
96
+ end
97
+
98
+ def assert_task_failed
99
+ assert_selector '.pf-v5-c-label', text: /Failed|Failure|Error/i, wait: 120
100
+ end
101
+
102
+ def assert_result_contains(text)
103
+ assert_selector '.pf-v5-c-code-block__code', text: text, wait: 15
104
+ end
105
+
106
+ def assert_result_has_content
107
+ assert_no_selector '.pf-v5-c-empty-state', text: 'No result data', wait: 15
108
+ assert_selector '.pf-v5-c-code-block__code', wait: 15
109
+ end
110
+
111
+ def assert_log_contains(text)
112
+ # Click the "Log Output" tab to see the bolt command and log
113
+ find('.pf-v5-c-tabs__link', text: 'Log Output').click
114
+ assert_selector '.pf-v5-c-code-block__code', text: text, wait: 15
115
+ end
116
+
117
+ # OpenBolt options and task parameters both use param_ prefix for field IDs
118
+ def set_openbolt_option(name, value)
119
+ field = find("#param_#{name}", wait: 10)
120
+ if field.tag_name == 'select'
121
+ field.select value
122
+ elsif field['type'] == 'checkbox'
123
+ value ? field.check : field.uncheck
124
+ else
125
+ field.fill_in with: value
126
+ end
127
+ end
128
+
129
+ # --- Settings page helpers ---
130
+
131
+ # Update a Foreman setting through the /settings UI. Each setting row
132
+ # has an inline edit button with id=<setting_name> that reveals an
133
+ # input with id=setting-input-<setting_name> and a submit button with
134
+ # ouiaId=submit-edit-btn (see Foreman's SettingValueCell /
135
+ # SettingValueEdit components).
136
+ def update_foreman_setting(setting_name, new_value, category: 'openbolt')
137
+ visit '/settings'
138
+ find("a[href='##{category}_settings_tab']", wait: 10).click
139
+
140
+ within("##{category}_settings_tab", wait: 10) do
141
+ find("button##{setting_name}", wait: 10).click
142
+ find("#setting-input-#{setting_name}", wait: 10).fill_in with: new_value
143
+ find("[data-ouia-component-id='submit-edit-btn']").click
144
+ end
145
+ end
146
+ end
@@ -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