<?php /* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; ** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ** See the GNU Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ require_once dirname(__FILE__) . '/../../include/CWebTest.php'; require_once dirname(__FILE__).'/../behaviors/CMessageBehavior.php'; require_once dirname(__FILE__).'/../behaviors/CTableBehavior.php'; require_once dirname(__FILE__).'/../../include/helpers/CDataHelper.php'; /** * Test for checking Proxies page. * * @dataSource Proxies * * @backup hosts */ class testPageAdministrationProxies extends CWebTest { /** * Attach MessageBehavior and TableBehavior to the test. * * @return array */ public function getBehaviors() { return [ CMessageBehavior::class, CTableBehavior::class ]; } private $sql = 'SELECT * FROM hosts ORDER BY hostid'; public function testPageAdministrationProxies_Layout() { $this->page->login()->open('zabbix.php?action=proxy.list')->waitUntilReady(); $this->page->assertTitle('Configuration of proxies'); $this->page->assertHeader('Proxies'); $form = $this->query('name:zbx_filter')->waitUntilPresent()->asForm()->one(); // Check default fields and values. $fields = [ 'Name' => [''], 'Mode' => ['Any', 'Active', 'Passive'], 'Version' => ['Any', 'Current', 'Outdated'] ]; foreach ($fields as $field => $value) { $this->assertEquals($value[0], $form->getField($field)->getValue()); } array_shift($fields); foreach ($fields as $radio => $labels) { $this->assertEquals($labels, $form->getField($radio)->asSegmentedRadio()->getLabels()->asText()); } // Check filter collapse/expand. foreach ([true, false] as $status) { $this->assertEquals($status, $this->query('xpath://div[contains(@class, "ui-tabs-panel")]')->one()->isVisible()); $this->query('xpath://a[contains(@class, "filter-trigger")]')->one()->click(); } $table = $this->query('class:list-table')->asTable()->one()->waitUntilPresent(); $this->assertEquals(['', 'Name', 'Mode', 'Encryption', 'State', 'Version', 'Last seen (age)', 'Item count', 'Required vps', 'Hosts'], $table->getHeadersText() ); // Check versions and hints. $versions = [ 'active_current' => ['version' => '6.4.0'], 'active_unknown' => ['version' => ''], 'passive_outdated' => ['version' => '6.2.0 ', 'color' => 'red', 'icon_color' => 'zi-i-warning', 'hint_text' => 'Proxy version is outdated, only data collection and remote execution is available with server version 6.4.0.' ], 'passive_unsupported' => ['version' => '5.4.1 ', 'color' => 'red', 'icon_color' => 'zi-i-negative', 'hint_text' => 'Proxy version is not supported by server version 6.4.0.', 'hint_color' => 'red' ] ]; foreach ($versions as $proxy => $parameters) { $column = $table->findRow('Name', $proxy, true)->getColumn('Version'); $this->assertEquals($parameters['version'], $column->getText()); if (array_key_exists('color', $parameters)) { // Check version text color. $this->assertTrue($column->query("xpath:.//span[@class=". CXPathHelper::escapeQuotes($parameters['color'])."]")->exists() ); // Check info-icon color. $this->assertTrue($column->query("xpath:.//button[".CXPathHelper::fromClass($parameters['icon_color'])."]") ->exists() ); // Check version hint. $column->query('xpath:.//button[@data-hintbox="1"]')->one()->waitUntilClickable()->click(); $hint = $this->query('xpath://div[@class="overlay-dialogue wordbreak"]')->waitUntilVisible()->one(); $this->assertEquals($parameters['hint_text'], $hint->getText()); if (array_key_exists('hint_color', $parameters)) { $this->assertTrue($hint->query("xpath:.//div[@class=". CXPathHelper::escapeQuotes("hintbox-wrap ".$parameters['hint_color'])."]")->exists() ); } $hint->asOverlayDialog()->close(); } else { // Check that info-icon is absent. $this->assertFalse($column->query('xpath:.//a[@class="rel-container"]')->exists()); } } // Check buttons disabled by default. foreach (['Refresh configuration', 'Enable hosts', 'Disable hosts', 'Delete'] as $button) { $this->assertTrue($this->query('button', $button)->one()->isVisible()); $this->assertFalse($this->query('button', $button)->one()->isClickable()); } } public function testPageAdministrationProxies_CheckTableAndFilterReset() { $this->page->login()->open('zabbix.php?action=proxy.list')->waitUntilReady(); $form = $this->query('name:zbx_filter')->waitUntilPresent()->asForm()->one(); // Reset filter in case if some filtering remained before ongoing test case. $form->query('button:Reset')->one()->click(); $table = $this->query('class:list-table')->asTable()->one()->waitUntilPresent(); $rows_count = $table->getRows()->count(); $table_contents = $this->getTableColumnData('Name'); // Fill filter form with data. $form->fill(['Name' => 'proxy1']); $form->submit(); $this->page->waitUntilReady(); $filter_result = [ [ 'Name' => 'active_proxy1', 'Mode' => 'Active', 'Encryption' => 'None', 'Version' => '', 'State' => 'Unknown', 'Last seen (age)' => 'Never', 'Item count' => '', 'Required vps' => '', 'Hosts' => '1' ], [ 'Name' => 'Degrading proxy group: passive_proxy1', 'Mode' => 'Passive', 'Encryption' => 'None', 'Version' => '', 'State' => 'Unknown', 'Last seen (age)' => 'Never', 'Item count' => '', 'Required vps' => '', 'Hosts' => '1' ] ]; $hosts = [ 'active_proxy1' => 'enabled_host1', 'Degrading proxy group: passive_proxy1' => 'disabled_host1' ]; // Check filtered result. $this->assertTableData($filter_result); foreach ($hosts as $proxy_name => $host_name) { $this->assertEquals($host_name, $table->findRow('Name', $proxy_name)->getColumn(10)->getText()); } // Reset filter and assert row count. $form->query('button:Reset')->one()->click(); $this->assertEquals($rows_count, $table->getRows()->count()); $this->assertEquals($table_contents, $this->getTableColumnData('Name')); } public function getFilterProxyData() { return [ [ [ 'filter' => [ 'Name' => 'for filter' ], 'result' => [ 'Online proxy group: Proxy_1 for filter', 'Online proxy group: Proxy_2 for filter' ] ] ], [ [ 'filter' => [ 'Name' => 'Active proxy' ], 'result' => [ 'Online proxy group: Active proxy 1', 'Online proxy group: Active proxy 2', 'Online proxy group: Active proxy 3', 'Online proxy group: Active proxy to delete' ], 'check_stats' => true ] ], [ [ 'filter' => [ 'Name' => 'Passive proxy' ], 'result' => [ 'Degrading proxy group: Passive proxy 1', 'Passive proxy 2', 'Passive proxy 3', 'Passive proxy to delete' ] ] ], [ [ 'filter' => [ 'Name' => 'filter', 'Mode' => 'Active' ], 'result' => [ 'Online proxy group: Proxy_1 for filter', 'Online proxy group: Proxy_2 for filter' ] ] ], [ [ 'filter' => [ 'Name' => 'filter', 'Mode' => 'Passive' ], 'result' => [] ] ], [ [ 'filter' => [ 'Name' => 'Active', 'Mode' => 'Active', 'Version' => 'Current' ], 'result' => [ 'active_current' ] ] ], [ [ 'filter' => [ 'Name' => '', 'Mode' => 'Passive', 'Version' => 'Outdated' ], 'result' => [ '⭐️😀⭐Smiley प्रॉक्सी 团体⭐️😀⭐ - unknown: passive_outdated', 'Degrading proxy group: passive_unsupported' ] ] ] ]; } /** * @dataProvider getFilterProxyData */ public function testPageAdministrationProxies_Filter($data) { $this->page->login()->open('zabbix.php?action=proxy.list')->waitUntilReady(); $form = $this->query('name:zbx_filter')->waitUntilPresent()->asForm()->one(); // Reset filter in case if some filtering remained before ongoing test case. $form->query('button:Reset')->one()->click(); // Fill filter form with data. $form->fill($data['filter']); $form->submit(); $this->page->waitUntilReady(); // Check filtered result. $this->assertTableDataColumn($data['result']); if (CTestArrayHelper::get($data, 'check_stats')) { $this->assertTableStats(count($data['result'])); } // Reset filter not to impact the results of next tests. $this->query('button:Reset')->one()->click(); } public static function getActionsProxyData() { return [ // Refresh of one active proxy. [ [ 'action' => 'Refresh configuration', 'proxies' => [ '2nd Online proxy group: active_proxy5' ], 'alert' => 'Refresh configuration of the selected proxy?', 'title' => 'Request created successfully' ] ], // Refresh of one passive proxy. [ [ 'action' => 'Refresh configuration', 'proxies' => [ 'passive_proxy5' ], 'alert' => 'Refresh configuration of the selected proxy?', 'title' => 'Request created successfully' ] ], // Refresh of one proxy with hosts. [ [ 'action' => 'Refresh configuration', 'proxies' => [ 'passive_proxy4' ], 'alert' => 'Refresh configuration of the selected proxy?', 'title' => 'Request created successfully' ] ], // Refresh of one proxy used by discovery rule. [ [ 'action' => 'Refresh configuration', 'proxies' => [ 'Degrading proxy group: Passive proxy 1' ], 'alert' => 'Refresh configuration of the selected proxy?', 'title' => 'Request created successfully' ] ], // Mass refresh of several proxies. [ [ 'action' => 'Refresh configuration', 'proxies' => [ 'passive_proxy5', 'active_proxy4', 'Degrading proxy group: Passive proxy 1' ], 'alert' => 'Refresh configuration of the selected proxies?', 'title' => 'Request created successfully' ] ], // Enable 1 enabled host on 1 proxy. [ [ 'action' => 'Enable hosts', 'proxies' => [ 'active_proxy1' ], 'hosts' => [ 'enabled_host1' ], 'alert' => 'Enable hosts monitored by selected proxy?', 'title' => 'Hosts enabled' ] ], // Disable 1 disabled host on 1 proxy. [ [ 'action' => 'Disable hosts', 'proxies' => [ 'Degrading proxy group: passive_proxy1' ], 'hosts' => [ 'disabled_host1' ], 'alert' => 'Disable hosts monitored by selected proxy?', 'title' => 'Hosts disabled' ] ], // Enable 1 enabled and 1 disabled hosts on 2 proxies. [ [ 'action' => 'Enable hosts', 'proxies' => [ 'active_proxy1', 'Degrading proxy group: passive_proxy1' ], 'hosts' => [ 'enabled_host1', 'disabled_host1' ], 'alert' => 'Enable hosts monitored by selected proxies?', 'title' => 'Host enabled', 'message' => 'Updated status of host "disabled_host1".' ] ], // Enable 2 disabled and 2 enabled hosts on 2 proxies. [ [ 'action' => 'Enable hosts', 'proxies' => [ '2nd Online proxy group: active_proxy3', 'passive_proxy2' ], 'hosts' => [ 'disabled_host2', 'disabled_host3', 'enabled_host4', 'enabled_host5' ], 'alert' => 'Enable hosts monitored by selected proxies?', 'title' => 'Hosts enabled', 'message' => [ 'Updated status of host "disabled_host2".', 'Updated status of host "disabled_host3".' ] ] ], // Disable 3 disabled and 3 enabled hosts on 2 proxies. [ [ 'action' => 'Disable hosts', 'proxies' => [ 'active_proxy4', 'passive_proxy4' ], 'hosts' => [ 'enabled_host6', 'disabled_host6', 'enabled_host7', 'enabled_host8', 'disabled_host7', 'disabled_host8' ], 'alert' => 'Disable hosts monitored by selected proxies?', 'title' => 'Hosts disabled', 'message' => [ 'Updated status of host "enabled_host6".', 'Updated status of host "enabled_host7".', 'Updated status of host "enabled_host8".' ] ] ], // Delete active proxy. [ [ 'action' => 'Delete', 'proxies' => [ '2nd Online proxy group: active_proxy5' ], 'alert' => 'Delete selected proxy?', 'title' => 'Proxy deleted' ] ], // Delete passive proxy. [ [ 'action' => 'Delete', 'proxies' => [ 'passive_proxy5' ], 'alert' => 'Delete selected proxy?', 'title' => 'Proxy deleted' ] ], // Mass delete of active and passive proxies. [ [ 'action' => 'Delete', 'proxies' => [ 'active_proxy6', 'passive_proxy6' ], 'alert' => 'Delete selected proxies?', 'title' => 'Proxies deleted' ] ], // Mass delete when one of the proxies monitor some host. [ [ 'expected' => TEST_BAD, 'action' => 'Delete', 'proxies' => [ 'active_proxy1', 'Offline group: active_proxy7' ], 'alert' => 'Delete selected proxies?', 'title' => 'Cannot delete proxies', 'error' => 'Host "enabled_host1" is monitored by proxy "active_proxy1".' ] ], // Mass delete when one of the proxies monitor is used by discovery rule. [ [ 'expected' => TEST_BAD, 'action' => 'Delete', 'proxies' => [ 'Default values - recovering: passive_proxy7', 'Delete Proxy used in Network discovery rule' ], 'alert' => 'Delete selected proxies?', 'title' => 'Cannot delete proxies', 'error' => "Proxy \"Delete Proxy used in Network discovery rule\" is used by discovery rule ". "\"Discovery rule for proxy delete test\"." ] ], // Delete one proxy with host. [ [ 'expected' => TEST_BAD, 'action' => 'Delete', 'proxies' => [ 'Online proxy group: Proxy_2 for filter' ], 'alert' => 'Delete selected proxy?', 'title' => 'Cannot delete proxy', 'error' => 'Host "Host_2 with proxy" is monitored by proxy "Proxy_2 for filter".' ] ], // Delete one proxy used by discovery rule. [ [ 'expected' => TEST_BAD, 'action' => 'Delete', 'proxies' => [ 'Delete Proxy used in Network discovery rule' ], 'alert' => 'Delete selected proxy?', 'title' => 'Cannot delete proxy', 'error' => "Proxy \"Delete Proxy used in Network discovery rule\" is used by discovery rule ". "\"Discovery rule for proxy delete test\"." ] ] ]; } /** * @dataProvider getActionsProxyData */ public function testPageAdministrationProxies_Actions($data) { if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) { $old_hash = CDBHelper::getHash($this->sql); } $this->page->login()->open('zabbix.php?action=proxy.list')->waitUntilReady(); $this->query('class:list-table')->asTable()->one()->findRows('Name', $data['proxies'])->select(); $this->query('button', $data['action'])->waitUntilClickable()->one()->click(); $this->assertTrue($this->page->isAlertPresent()); $this->assertEquals($data['alert'], $this->page->getAlertText()); $this->page->acceptAlert(); if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) { $this->assertMessage(TEST_BAD, $data['title'], $data['error']); // Check that DB hash is not changed. $this->assertEquals($old_hash, CDBHelper::getHash($this->sql)); } else { $this->assertMessage(TEST_GOOD, $data['title'], CTestArrayHelper::get($data, 'message', null)); // Check DB. Status 5 stands for Active proxy and status 6 - for Passive proxy. $db_proxies = CDBHelper::getColumn('SELECT * FROM proxy', 'name'); foreach ($data['proxies'] as $proxy) { /** * In the DB and in the link proxy name is without the group, so group name should be removed from proxy name. * If search for group name and proxy name delimiter is found in 'Name' column value, all is trimmed up * to the position of this delimiter and the following 2 symbols to remove the delimiter itself. */ $proxy_name = (($position = strpos($proxy, ': ')) !== false) ? substr($proxy, $position + 2) : $proxy; $this->assertEquals(($data['action'] !== 'Delete'), in_array($proxy_name, array_values($db_proxies))); $exists = ($data['action'] === 'Delete') ? array_key_exists('expected', $data) : true; $this->assertEquals($exists, $this->query('link', $proxy_name)->exists()); } // Check that hosts are actually enabled/disabled. if ($data['action'] === 'Enable hosts' || $data['action'] === 'Disable hosts') { $hosts = CDBHelper::getAll('SELECT host, status FROM hosts WHERE proxyid IS NOT NULL'); // DB check for hosts. foreach ($hosts as $host) { if (in_array($host['host'], $data['hosts'])) { $this->assertEquals(($data['action'] === 'Enable hosts') ? 0 : 1, $host['status']); } } } } // Check that user redirected on Proxies page. $this->page->assertHeader('Proxies'); } /** * @backup profiles */ public function testPageAdministrationProxies_SortColumns() { // Open Proxies page with proxies sorted descendingly by name. $this->page->login()->open('zabbix.php?action=proxy.list&sort=name&sortorder=DESC')->waitUntilReady(); $table = $this->query('class:list-table')->asTable()->one()->waitUntilPresent(); foreach (['Name', 'Mode', 'Encryption', 'Version', 'Last seen (age)'] as $column) { /** * Name column is sorted only not taking into account the proxy group name, so proxy group name should be * removed before sorting. Values with groups are saved in a separate array to restore proxy group names later. */ if ($column === 'Name') { $content_with_groups = $this->getTableColumnData($column); $proxy_count = count($content_with_groups); $content = []; foreach ($content_with_groups as $proxy_name) { $content[] = (($position = strpos($proxy_name, ': ')) !== false) ? substr($proxy_name, $position + 2) : $proxy_name; } } else { $content = $this->getTableColumnData($column); } $sorted_asc = $content; $sorted_desc = $content; // Sort column contents ascending. usort($sorted_asc, function($a, $b) { return strcasecmp($a, $b); }); // Sort column contents descending. usort($sorted_desc, function($a, $b) { return strcasecmp($b, $a); }); $arrays = [$sorted_asc, $sorted_desc]; /** * After proxy names are sorted, group names should be returned to the corresponding proxy names. * To do so, go through the previously saved array with full "Name" column values and for each value go through * all indexes. In case if array element with the corresponding index matches a part of the corresponding * full "Name" column value, then this full name is written into the sorted array with this index. */ if ($column === 'Name') { foreach ($arrays as &$sorted_array) { foreach ($content_with_groups as $name_with_group) { for ($i = 0; $i < $proxy_count; $i++) { if (str_contains($name_with_group, $sorted_array[$i])) { $sorted_array[$i] = $name_with_group; } } } } unset($sorted_array); }; // Check ascending and descending sorting in column. foreach ($arrays as $order) { $table->query('link', $column)->waitUntilClickable()->one()->click(); $table->waitUntilReloaded(); $this->assertTableDataColumn($order, $column); } } } }