diff --git a/CHANGELOG.md b/CHANGELOG.md index f7ff05c1..692d7ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [UNRELEASED] + +### Fixed + +- Fix massive action update on CustomAssets + ## [1.24.4] - 2026-08-06 ### Fixed diff --git a/hook.php b/hook.php index 409b9af2..bc200ccb 100644 --- a/hook.php +++ b/hook.php @@ -54,11 +54,11 @@ function plugin_fields_install() $migration = new Migration($version); if (isCommandLine()) { - echo __('MySQL tables installation', 'fields') . "\n"; + echo __s('MySQL tables installation', 'fields') . "\n"; } else { echo '
'; echo ""; - echo ''; + echo ''; echo ""; echo "
' . __('MySQL tables installation', 'fields') . '
' . __s('MySQL tables installation', 'fields') . '
"; @@ -111,7 +111,7 @@ function plugin_fields_uninstall() { if (!class_exists('PluginFieldsProfile')) { Session::addMessageAfterRedirect( - __("The plugin can't be uninstalled when the plugin is disabled", 'fields'), + __s("The plugin can't be uninstalled when the plugin is disabled", 'fields'), true, WARNING, true, @@ -124,7 +124,7 @@ function plugin_fields_uninstall() echo '
'; echo ""; - echo ''; + echo ''; echo ""; echo "
' . __('MySQL tables uninstallation', 'fields') . '
' . __s('MySQL tables uninstallation', 'fields') . '
"; @@ -227,9 +227,7 @@ function plugin_fields_MassiveActionsFieldsDisplay($options = []) ); } - PluginFieldsField::showSingle($options['itemtype'], $options['options'], true); - - return true; + return PluginFieldsField::showSingle($options['itemtype'], $options['options'], true); } // Need to return false on non display item diff --git a/inc/container.class.php b/inc/container.class.php index 0c0b2ebb..f98e3526 100644 --- a/inc/container.class.php +++ b/inc/container.class.php @@ -2097,12 +2097,23 @@ private static function populateData($c_id, CommonDBTM $item) //managed multi GLPI item dropdown field if (preg_match('/^dropdown-(?.+)$/', (string) $field['type'], $match) === 1) { + $defined_key = '_' . $field['name'] . '_defined'; //values are defined by user if (isset($item->input[$field['name']])) { $data[$field['name']] = $item->input[$field['name']]; $has_fields = true; - } else { //multi dropdown is empty or has been emptied + } elseif ( + isset($item->input[$defined_key]) + && $item->input[$defined_key] + ) { //multi dropdown is empty or has been emptied $data[$field['name']] = []; + $has_fields = true; + } elseif ( + isset($_REQUEST['massiveaction']) + && isset($_POST[$field['name']]) + ) { // called from massiveaction + $data[$field['name']] = $_POST[$field['name']]; + $has_fields = true; } } } diff --git a/inc/field.class.php b/inc/field.class.php index 07f25d69..ce9b3e24 100644 --- a/inc/field.class.php +++ b/inc/field.class.php @@ -1361,6 +1361,14 @@ public static function showSingle($itemtype, $searchOption, $massiveaction = fal (string) $searchOption['linkfield'], ); + // itemtype is stored in a JSON array, so entry is surrounded by double quotes + $search_string = json_encode($itemtype); + // Backslashes must be doubled in LIKE clause according to MySQL documentation + // But do not escape backslashes for namespaced itemtypes, as they are already escaped + if (!str_contains((string) $itemtype, '\\')) { + $search_string = str_replace('\\', '\\\\', $search_string); + } + //find field $iterator = $DB->request([ 'SELECT' => [ @@ -1381,7 +1389,7 @@ public static function showSingle($itemtype, $searchOption, $massiveaction = fal ], 'WHERE' => [ 'fields.name' => $cleaned_linkfield, - 'containers.itemtypes' => ['LIKE', sprintf('%%%s%%', $itemtype)], + 'containers.itemtypes' => ['LIKE', '%' . $DB->escape($search_string) . '%'], ], ]); diff --git a/tests/Units/MassiveActionCustomAssetTest.php b/tests/Units/MassiveActionCustomAssetTest.php new file mode 100644 index 00000000..7944d159 --- /dev/null +++ b/tests/Units/MassiveActionCustomAssetTest.php @@ -0,0 +1,107 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use Glpi\Tests\DbTestCase; +use Glpi\Tests\GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use PluginFieldsContainer; +use PluginFieldsField; +use Search; + +require_once __DIR__ . '/../FieldTestCase.php'; + +/** + * Reproduces the bug where the massive action "update" widget for a Fields + * plugin field is never rendered for CustomAsset itemtypes (namespaced + * classes like Glpi\CustomAsset\XxxAsset), because PluginFieldsField::showSingle() + * builds a LIKE query against the un-escaped itemtype string. + */ +final class MassiveActionCustomAssetTest extends DbTestCase +{ + use FieldTestTrait; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + } + + public function tearDown(): void + { + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + public function testShowSingleDisplaysFieldForCustomAsset(): void + { + $definition = $this->initAssetDefinition('so' . substr((string) $this->getUniqueString(), 0, 6)); + $asset_class = $definition->getAssetClassName(); + + $container = $this->createFieldContainer([ + 'label' => 'F', + 'type' => 'tab', + 'itemtypes' => [$asset_class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $field = $this->createField([ + 'label' => 'Custom Asset Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + $field_name = $field->fields['name']; + + $search_option = null; + foreach (Search::getOptions($asset_class) as $so) { + if (($so['linkfield'] ?? null) === $field_name) { + $search_option = $so; + break; + } + } + + $this->assertIsArray($search_option, 'search option not found for plugin field on custom asset'); + + ob_start(); + $result = PluginFieldsField::showSingle($asset_class, $search_option, true); + $html = ob_get_clean(); + + $this->assertTrue($result, 'showSingle() should find the field container for a CustomAsset itemtype'); + $this->assertStringContainsString($field_name, $html); + } +} diff --git a/tests/Units/MassiveActionGlpiItemDropdownTest.php b/tests/Units/MassiveActionGlpiItemDropdownTest.php new file mode 100644 index 00000000..12663d61 --- /dev/null +++ b/tests/Units/MassiveActionGlpiItemDropdownTest.php @@ -0,0 +1,193 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use PluginFieldsField; +use Glpi\Tests\DbTestCase; +use Glpi\Tests\GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use Location; +use MassiveAction; +use PluginFieldsContainer; +use ReflectionClass; +use Glpi\Search\SearchOption; + +require_once __DIR__ . '/../FieldTestCase.php'; + +/** + * Reproduces the bug where a massive action "update" of one field wipes a + * sibling "dropdown-" field (e.g. a multi-select referencing + * Location) in the same container, because PluginFieldsContainer::populateData() + * unconditionally blanks it to [] whenever it's absent from the current + * request's input - unlike the plain "dropdown" type, which is guarded + * against this (fixed in #795/#974). + */ +final class MassiveActionGlpiItemDropdownTest extends DbTestCase +{ + use FieldTestTrait; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + } + + public function tearDown(): void + { + unset($_REQUEST['massiveaction'], $_POST); + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + private function buildMassiveAction(array $post, string $itemtype, int $id): MassiveAction + { + $ref = new ReflectionClass(MassiveAction::class); + $ma = $ref->newInstanceWithoutConstructor(); + $ma->POST = $post; + + $set = static function (string $name, $value) use ($ref, $ma): void { + $prop = $ref->getProperty($name); + $prop->setValue($ma, $value); + }; + + $set('action', 'update'); + $set('done', []); + $set('nb_done', 0); + $set('current_itemtype', null); + $set('remainings', [$itemtype => [$id => $id]]); + $set('results', ['ok' => 0, 'noaction' => 0, 'ko' => 0, 'noright' => 0, 'messages' => []]); + $set('start_time', microtime(true)); + + return $ma; + } + + public function testMassiveUpdateOfOtherFieldDoesNotWipeGlpiItemDropdown(): void + { + $definition = $this->initAssetDefinition('so' . substr((string) $this->getUniqueString(), 0, 6)); + $asset_class = $definition->getAssetClassName(); + + $container = $this->createFieldContainer([ + 'label' => 'F', + 'type' => 'tab', + 'itemtypes' => [$asset_class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $glpi_item_field = $this->createItem(PluginFieldsField::class, [ + 'label' => 'Locations', + 'type' => 'dropdown-' . Location::class, + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ], ['allowed_values', 'question_types', 'default_value']); + $glpi_item_field_name = $glpi_item_field->fields['name']; + + $text_field = $this->createField([ + 'label' => 'Other Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 2, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + $text_field_name = $text_field->fields['name']; + + $location1 = $this->createItem(Location::class, ['name' => $this->getUniqueString(), 'entities_id' => 0]); + $location2 = $this->createItem(Location::class, ['name' => $this->getUniqueString(), 'entities_id' => 0]); + + $entities_id = reset($_SESSION['glpiactiveentities']); + $asset = $this->createItem($asset_class, ['name' => 'Test asset', 'entities_id' => $entities_id]); + + $so = SearchOption::getOptionsForItemtype($asset_class); + $glpi_item_so_index = null; + $text_so_index = null; + foreach ($so as $idx => $opt) { + if (($opt['linkfield'] ?? null) === $glpi_item_field_name || ($opt['field'] ?? null) === $glpi_item_field_name) { + $glpi_item_so_index = $idx; + } + + if (($opt['linkfield'] ?? null) === $text_field_name) { + $text_so_index = $idx; + } + } + + $this->assertNotNull($glpi_item_so_index, 'search option index not found for dropdown-Location field'); + $this->assertNotNull($text_so_index, 'search option index not found for text field'); + + $_REQUEST['massiveaction'] = 1; + + // --- First massive action: set the multi-select Location field --- + $_POST = [ + 'common_options' => [$asset_class . ':' . $glpi_item_so_index => $asset_class . ':' . $glpi_item_so_index], + 'search_options' => [$asset_class => $glpi_item_so_index], + 'id_field' => $asset_class . ':' . $glpi_item_so_index, + 'field' => $glpi_item_field_name, + $glpi_item_field_name => [$location1->getID(), $location2->getID()], + ]; + $ma1 = $this->buildMassiveAction($_POST, $asset_class, $asset->getID()); + MassiveAction::processMassiveActionsForOneItemtype($ma1, $asset, [$asset->getID()]); + + $container_classname = PluginFieldsContainer::getClassname($asset_class, $container->fields['name']); + $container_item = new $container_classname(); + $this->assertTrue($container_item->getFromDBByCrit(['items_id' => $asset->getID()])); + $this->assertSame( + [$location1->getID(), $location2->getID()], + json_decode($container_item->fields[$glpi_item_field_name], true), + 'dropdown-Location value not correctly stored after first massive action', + ); + + // --- Second massive action: update the sibling text field only --- + $_POST = [ + 'common_options' => [$asset_class . ':' . $text_so_index => $asset_class . ':' . $text_so_index], + 'search_options' => [$asset_class => $text_so_index], + 'id_field' => $asset_class . ':' . $text_so_index, + 'field' => $text_field_name, + $text_field_name => 'hello', + ]; + $ma2 = $this->buildMassiveAction($_POST, $asset_class, $asset->getID()); + MassiveAction::processMassiveActionsForOneItemtype($ma2, $asset, [$asset->getID()]); + + $container_item2 = new $container_classname(); + $this->assertTrue($container_item2->getFromDBByCrit(['items_id' => $asset->getID()])); + $this->assertSame( + [$location1->getID(), $location2->getID()], + json_decode($container_item2->fields[$glpi_item_field_name], true), + 'Massive-updating the sibling text field must not wipe the previously set dropdown-Location value', + ); + } +} diff --git a/tests/Units/MassiveActionMultipleDropdownTest.php b/tests/Units/MassiveActionMultipleDropdownTest.php new file mode 100644 index 00000000..7a6fbe50 --- /dev/null +++ b/tests/Units/MassiveActionMultipleDropdownTest.php @@ -0,0 +1,148 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use PluginFieldsField; +use Glpi\Tests\DbTestCase; +use Glpi\Tests\GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use PluginFieldsContainer; +use PluginFieldsDropdown; + +require_once __DIR__ . '/../FieldTestCase.php'; + +/** + * Reproduces the bug where a massive action "update" of one field wipes out + * a previously mass-updated multiple-value dropdown field in the same + * container, because PluginFieldsContainer::populateData() re-evaluates + * every field of the container on every update, not just the field being + * edited. + */ +final class MassiveActionMultipleDropdownTest extends DbTestCase +{ + use FieldTestTrait; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + } + + public function tearDown(): void + { + unset($_REQUEST['massiveaction']); + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + public function testMassiveUpdateOfOtherFieldDoesNotWipeMultipleDropdown(): void + { + $definition = $this->initAssetDefinition('so' . substr((string) $this->getUniqueString(), 0, 6)); + $asset_class = $definition->getAssetClassName(); + + $container = $this->createFieldContainer([ + 'label' => 'F', + 'type' => 'tab', + 'itemtypes' => [$asset_class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + // Multiple-value Fields dropdown (e.g. "Transmission") + $dropdown_field = $this->createItem(PluginFieldsField::class, [ + 'label' => 'Transmission', + 'type' => 'dropdown', + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ], ['allowed_values', 'question_types', 'default_value']); + $dropdown_field_name = $dropdown_field->fields['name']; + $multiple_key = 'plugin_fields_' . $dropdown_field_name . 'dropdowns_id'; + + // A plain sibling field in the SAME container + $text_field = $this->createField([ + 'label' => 'Other Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 2, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + $text_field_name = $text_field->fields['name']; + + // Two allowed dropdown values + $dropdown_class = PluginFieldsDropdown::getClassname($dropdown_field_name); + $value1 = $this->createItem($dropdown_class, ['name' => 'Manual', 'entities_id' => 0]); + $value2 = $this->createItem($dropdown_class, ['name' => 'Automatic', 'entities_id' => 0]); + + $asset = $this->createItem($asset_class, ['name' => 'Test asset', 'entities_id' => 0]); + + $_REQUEST['massiveaction'] = 1; + + // First massive action: set the multiple dropdown field + $item = new $asset_class(); + $this->assertTrue($item->update([ + 'id' => $asset->getID(), + 'c_id' => $container->getID(), + $multiple_key => [$value1->getID(), $value2->getID()], + ])); + + $container_classname = PluginFieldsContainer::getClassname($asset_class, $container->fields['name']); + $container_item = new $container_classname(); + $this->assertTrue($container_item->getFromDBByCrit(['items_id' => $asset->getID()])); + $this->assertSame( + [$value1->getID(), $value2->getID()], + json_decode($container_item->fields[$multiple_key], true), + ); + + // Second massive action: update the sibling text field only + $item2 = new $asset_class(); + $this->assertTrue($item2->update([ + 'id' => $asset->getID(), + 'c_id' => $container->getID(), + $text_field_name => 'hello', + ])); + + $container_item2 = new $container_classname(); + $this->assertTrue($container_item2->getFromDBByCrit(['items_id' => $asset->getID()])); + $this->assertSame( + [$value1->getID(), $value2->getID()], + json_decode($container_item2->fields[$multiple_key], true), + 'Massive-updating the sibling text field must not wipe the previously set multiple dropdown value', + ); + } +} diff --git a/tests/Units/MassiveActionRealFlowTest.php b/tests/Units/MassiveActionRealFlowTest.php new file mode 100644 index 00000000..488809a2 --- /dev/null +++ b/tests/Units/MassiveActionRealFlowTest.php @@ -0,0 +1,193 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2013-2023 by Fields plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/fields + * ------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Field\Tests\Units; + +use PluginFieldsField; +use Glpi\Tests\DbTestCase; +use Glpi\Tests\GLPITestCase; +use GlpiPlugin\Field\Tests\FieldTestTrait; +use MassiveAction; +use PluginFieldsContainer; +use PluginFieldsDropdown; +use ReflectionClass; +use Glpi\Search\SearchOption; + +require_once __DIR__ . '/../FieldTestCase.php'; + +/** + * Reproduces the exact core MassiveAction::processMassiveActionsForOneItemtype() + * code path (the one producing "Array to string conversion" warnings) for a + * CustomAsset with a Fields-plugin multiple dropdown, then a second massive + * action on a sibling field, checking whether the dropdown value survives. + */ +final class MassiveActionRealFlowTest extends DbTestCase +{ + use FieldTestTrait; + + public function setUp(): void + { + GLPITestCase::setUp(); + $this->login(); + } + + public function tearDown(): void + { + unset($_REQUEST['massiveaction'], $_POST); + $this->tearDownFieldTest(); + GLPITestCase::tearDown(); + } + + private function buildMassiveAction(array $post, string $itemtype, int $id): MassiveAction + { + $ref = new ReflectionClass(MassiveAction::class); + $ma = $ref->newInstanceWithoutConstructor(); + $ma->POST = $post; + + $set = static function (string $name, $value) use ($ref, $ma): void { + $prop = $ref->getProperty($name); + $prop->setValue($ma, $value); + }; + + $set('action', 'update'); + $set('done', []); + $set('nb_done', 0); + $set('current_itemtype', null); + $set('remainings', [$itemtype => [$id => $id]]); + $set('results', ['ok' => 0, 'noaction' => 0, 'ko' => 0, 'noright' => 0, 'messages' => []]); + $set('start_time', microtime(true)); + + return $ma; + } + + public function testMassiveUpdateOfOtherFieldDoesNotWipeMultipleDropdown(): void + { + $definition = $this->initAssetDefinition('so' . substr((string) $this->getUniqueString(), 0, 6)); + $asset_class = $definition->getAssetClassName(); + + $container = $this->createFieldContainer([ + 'label' => 'F', + 'type' => 'tab', + 'itemtypes' => [$asset_class], + 'is_active' => 1, + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $dropdown_field = $this->createItem(PluginFieldsField::class, [ + 'label' => 'Transmission', + 'type' => 'dropdown', + 'multiple' => 1, + 'default_value' => [], + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 1, + 'is_active' => 1, + 'is_readonly' => 0, + ], ['allowed_values', 'question_types', 'default_value']); + $dropdown_field_name = $dropdown_field->fields['name']; + $multiple_key = 'plugin_fields_' . $dropdown_field_name . 'dropdowns_id'; + + $text_field = $this->createField([ + 'label' => 'Other Field', + 'type' => 'text', + PluginFieldsContainer::getForeignKeyField() => $container->getID(), + 'ranking' => 2, + 'is_active' => 1, + 'is_readonly' => 0, + ]); + $text_field_name = $text_field->fields['name']; + + $dropdown_class = PluginFieldsDropdown::getClassname($dropdown_field_name); + $value1 = $this->createItem($dropdown_class, ['name' => 'Manual', 'entities_id' => 0]); + $value2 = $this->createItem($dropdown_class, ['name' => 'Automatic', 'entities_id' => 0]); + + $entities_id = reset($_SESSION['glpiactiveentities']); + $asset = $this->createItem($asset_class, ['name' => 'Test asset', 'entities_id' => $entities_id]); + + $so = SearchOption::getOptionsForItemtype($asset_class); + $dropdown_so_index = null; + $text_so_index = null; + foreach ($so as $idx => $opt) { + if (($opt['linkfield'] ?? null) === $multiple_key || ($opt['field'] ?? null) === $multiple_key) { + $dropdown_so_index = $idx; + } + + if (($opt['linkfield'] ?? null) === $text_field_name) { + $text_so_index = $idx; + } + } + + $this->assertNotNull($dropdown_so_index, 'search option index not found for dropdown field'); + $this->assertNotNull($text_so_index, 'search option index not found for text field'); + + $_REQUEST['massiveaction'] = 1; + + // --- First massive action: update the multiple dropdown field --- + $_POST = [ + 'common_options' => [$asset_class . ':' . $dropdown_so_index => $asset_class . ':' . $dropdown_so_index], + 'search_options' => [$asset_class => $dropdown_so_index], + 'id_field' => $asset_class . ':' . $dropdown_so_index, + 'field' => $multiple_key, + $multiple_key => [$value1->getID(), $value2->getID()], + ]; + $ma1 = $this->buildMassiveAction($_POST, $asset_class, $asset->getID()); + MassiveAction::processMassiveActionsForOneItemtype($ma1, $asset, [$asset->getID()]); + + $container_classname = PluginFieldsContainer::getClassname($asset_class, $container->fields['name']); + $container_item = new $container_classname(); + $this->assertTrue($container_item->getFromDBByCrit(['items_id' => $asset->getID()])); + $this->assertSame( + [$value1->getID(), $value2->getID()], + json_decode($container_item->fields[$multiple_key], true), + 'dropdown value not correctly stored after first massive action', + ); + + // --- Second massive action: update the sibling text field only --- + $_POST = [ + 'common_options' => [$asset_class . ':' . $text_so_index => $asset_class . ':' . $text_so_index], + 'search_options' => [$asset_class => $text_so_index], + 'id_field' => $asset_class . ':' . $text_so_index, + 'field' => $text_field_name, + $text_field_name => 'hello', + ]; + $ma2 = $this->buildMassiveAction($_POST, $asset_class, $asset->getID()); + MassiveAction::processMassiveActionsForOneItemtype($ma2, $asset, [$asset->getID()]); + + $container_item2 = new $container_classname(); + $this->assertTrue($container_item2->getFromDBByCrit(['items_id' => $asset->getID()])); + $this->assertSame( + [$value1->getID(), $value2->getID()], + json_decode($container_item2->fields[$multiple_key], true), + 'Massive-updating the sibling text field must not wipe the previously set multiple dropdown value', + ); + } +}