diff --git a/src/Feeds/Target/ConfigEntityReference.php b/src/Feeds/Target/ConfigEntityReference.php new file mode 100644 index 00000000..68f1bbb4 --- /dev/null +++ b/src/Feeds/Target/ConfigEntityReference.php @@ -0,0 +1,324 @@ +entityTypeManager = $entity_type_manager; + $this->entityRepository = $entity_repository; + $this->transliteration = $transliteration; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity.repository'), + $container->get('transliteration') + ); + } + + /** + * {@inheritdoc} + */ + protected static function prepareTarget(FieldDefinitionInterface $field_definition) { + $type = $field_definition->getSetting('target_type'); + if (!\Drupal::entityTypeManager()->getDefinition($type)->entityClassImplements(ConfigEntityInterface::class)) { + return; + } + + return FieldTargetDefinition::createFromFieldDefinition($field_definition) + ->addProperty('target_id'); + } + + /** + * {@inheritdoc} + */ + public function setTarget(FeedInterface $feed, EntityInterface $entity, $field_name, array $raw_values) { + $values = []; + foreach ($raw_values as $delta => $columns) { + try { + $this->prepareValue($delta, $columns); + $values[] = $columns; + } + catch (ReferenceNotFoundException $e) { + // The referenced entity is not found. We need to enforce Feeds to try + // to import the same item again on the next import. + // Feeds stores a hash of every imported item in order to make the + // import process more efficient by ignoring items it has already seen. + // In this case we need to destroy the hash in order to be able to + // import the reference on a next import. + $entity->get('feeds_item')->hash = NULL; + $feed->getState(StateInterface::PROCESS)->setMessage($e->getFormattedMessage(), 'warning', TRUE); + } + catch (EmptyFeedException $e) { + // Nothing wrong here. + } + catch (TargetValidationException $e) { + // Validation failed. + $this->addMessage($e->getFormattedMessage(), 'error'); + } + } + + if (!empty($values)) { + $entity_target = $this->getEntityTarget($feed, $entity); + if ($entity_target) { + $item_list = $entity_target->get($field_name); + + // Append these values to the existing values. + $values = array_merge($item_list->getValue(), $values); + + $item_list->setValue($values); + } + } + } + + /** + * Returns possible fields to reference by for a config entity. + * + * @return array + * A list of fields to reference by. + */ + protected function getPotentialFields() { + /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $config_entity_type */ + $config_entity_type = $this->entityTypeManager->getDefinition($this->getEntityType()); + $config_name = $config_entity_type->getConfigPrefix() . '.*'; + $definition = \Drupal::service('config.typed')->getDefinition($config_name); + + if (!empty($definition['mapping'])) { + $options = []; + foreach ($definition['mapping'] as $key => $mapper) { + switch ($mapper['type']) { + case 'integer': + case 'label': + case 'string': + case 'text': + case 'uuid': + $options[$key] = $mapper['label']; + break; + } + } + return $options; + } + + return [ + 'id' => $this->t('ID'), + 'uuid' => $this->t('UUID'), + ]; + } + + /** + * Returns the entity type that can be referenced. + * + * @return string + * The entity type being referenced. + */ + protected function getEntityType() { + return $this->settings['target_type']; + } + + /** + * {@inheritdoc} + */ + protected function prepareValue($delta, array &$values) { + // Check if there is a value for target ID. + if (!isset($values['target_id']) || strlen(trim($values['target_id'])) === 0) { + // No value. + throw new EmptyFeedException(); + } + + if ($target_id = $this->findEntity($values['target_id'], $this->configuration['reference_by'])) { + $values['target_id'] = $target_id; + return; + } + + throw new ReferenceNotFoundException($this->t('Referenced entity not found for field %field with value %target_id.', [ + '%target_id' => $values['target_id'], + '%field' => $this->configuration['reference_by'], + ])); + } + + /** + * Searches for an entity by entity key. + * + * @param string $value + * The value to search for. + * @param string $field + * The subfield to search in. + * + * @return int|bool + * The entity id, or false, if not found. + */ + protected function findEntity($value, $field) { + switch ($field) { + case 'uuid': + if (NULL !== ($entity = $this->entityRepository->loadEntityByUuid($this->getEntityType(), $value))) { + return $entity->id(); + } + break; + + default: + $ids = $this->entityTypeManager->getStorage($this->getEntityType()) + ->getQuery() + ->condition($field, $value) + ->range(0, 1) + ->execute(); + + if ($ids) { + return reset($ids); + } + } + + return FALSE; + } + + /** + * Generates a machine name from a string. + * + * This is basically the same as what is done in + * \Drupal\Core\Block\BlockBase::getMachineNameSuggestion() and + * \Drupal\system\MachineNameController::transliterate(), but it seems + * that so far there is no common service for handling this. + * + * @param string $string + * The string to generate a machine name for. + * + * @return string + * The generated machine name. + * + * @see \Drupal\Core\Block\BlockBase::getMachineNameSuggestion() + * @see \Drupal\system\MachineNameController::transliterate() + */ + protected function generateMachineName($string) { + $transliterated = $this->transliteration->transliterate($string, LanguageInterface::LANGCODE_DEFAULT, '_'); + $transliterated = mb_strtolower($transliterated); + + $transliterated = preg_replace('@[^a-z0-9_.]+@', '_', $transliterated); + + return $transliterated; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $config = [ + 'reference_by' => 'id', + ]; + return $config; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $options = $this->getPotentialFields(); + + // Hack to find out the target delta. + foreach ($form_state->getValues() as $key => $value) { + if (strpos($key, 'target-settings-') === 0) { + list(, , $delta) = explode('-', $key); + break; + } + } + + $form['reference_by'] = [ + '#type' => 'select', + '#title' => $this->t('Reference by'), + '#options' => $options, + '#default_value' => $this->configuration['reference_by'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + $options = $this->getPotentialFields(); + + $summary = []; + + if ($this->configuration['reference_by'] && isset($options[$this->configuration['reference_by']])) { + $summary[] = $this->t('Reference by: %message', ['%message' => $options[$this->configuration['reference_by']]]); + } + else { + $summary[] = $this->t('Please select a field to reference by.'); + } + + return $summary; + } + +} diff --git a/src/Feeds/Target/UserRole.php b/src/Feeds/Target/UserRole.php new file mode 100644 index 00000000..191b2da1 --- /dev/null +++ b/src/Feeds/Target/UserRole.php @@ -0,0 +1,222 @@ +getProcessor(); + + if (!$processor instanceof EntityProcessorInterface) { + return $targets; + } + + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($processor->entityType(), $processor->bundle()); + + foreach ($field_definitions as $id => $field_definition) { + if ($field_definition->getType() == 'entity_reference' && $field_definition->getSetting('target_type') == 'user_role') { + if ($target = static::prepareTarget($field_definition)) { + $target->setPluginId($definition['id']); + $targets[$id] = $target; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function setTarget(FeedInterface $feed, EntityInterface $entity, $field_name, array $values) { + // Check if values list is currently empty. + $entity_target = $this->getEntityTarget($feed, $entity); + $is_empty = empty($entity_target->get($field_name)->getValue()); + + if (empty($entity_target)) { + return; + } + + parent::setTarget($feed, $entity, $field_name, $values); + + $item_list = $entity_target->get($field_name); + + // Append roles from unsaved entity, if there is one. + if ($entity_target->id() && $is_empty) { + $original = $this->entityTypeManager->getStorage($entity_target->getEntityTypeId()) + ->loadUnchanged($entity->id()); + if ($original) { + $original_values = $original->get($field_name)->getValue(); + + // Revoke roles, when that option is enabled. But do not touch roles + // that are not allowed to set by the source. + if ($this->configuration['revoke_roles']) { + foreach ($original_values as $key => $value) { + $rid = $value['target_id']; + if (!empty($this->configuration['allowed_roles'][$rid])) { + unset($original_values[$key]); + } + } + } + + // Merge the remaining values. + $values = array_merge($item_list->getValue(), $original_values); + + $item_list->setValue($values); + } + } + } + + /** + * {@inheritdoc} + */ + protected function findEntity($value, $field) { + $entity_id = parent::findEntity($value, $field); + if ($entity_id !== FALSE) { + // Check if the role may be assigned. + if (isset($this->configuration['allowed_roles'][$entity_id]) && !$this->configuration['allowed_roles'][$entity_id]) { + // This role may *not* be assiged. + return FALSE; + } + + return $entity_id; + } + + // Automatically create a new role. + if ($this->configuration['autocreate'] && in_array($this->configuration['reference_by'], ['id', 'name'])) { + return $this->createRole($value); + } + } + + /** + * Creates a new role with the given label and saves it. + * + * @param string $label + * The label the new role should get. + * + * @return int|string|false + * The ID of the new role or false if the given label is empty. + */ + protected function createRole($label) { + if (!strlen(trim($label))) { + return FALSE; + } + + $values = [ + 'id' => $this->generateMachineName($label), + 'name' => $label, + ]; + $entity = $this->entityTypeManager->getStorage($this->getEntityType())->create($values); + + $entity->save(); + + return $entity->id(); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $role_names = array_keys($this->getRoleNames()); + + $config = parent::defaultConfiguration() + [ + 'allowed_roles' => array_combine($role_names, $role_names), + 'autocreate' => FALSE, + 'revoke_roles' => FALSE, + ]; + return $config; + } + + /** + * Returns a list of role names, keyed by role ID. + * + * @return array + * A list of role names. + */ + protected function getRoleNames() { + $roles = $this->entityTypeManager->getStorage('user_role')->loadMultiple(); + unset($roles[RoleInterface::ANONYMOUS_ID]); + unset($roles[RoleInterface::AUTHENTICATED_ID]); + + return array_map(function ($item) { + return $item->label(); + }, $roles); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + $form['allowed_roles'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Allowed roles'), + '#options' => $this->getRoleNames(), + '#default_value' => $this->configuration['allowed_roles'], + '#description' => $this->t('Select the roles to accept from the feed.
Any other roles will be ignored.'), + ]; + $form['autocreate'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Auto create'), + '#description' => $this->t("Create the role if it doesn't exist."), + '#default_value' => $this->configuration['autocreate'], + ]; + $form['revoke_roles'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Revoke roles'), + '#description' => t('If enabled, roles that are not provided by the feed will be revoked for the user. This affects only the "Allowed roles" as configured above.'), + '#default_value' => $this->configuration['revoke_roles'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + $summary = parent::getSummary(); + + // Allowed roles. + $role_names = array_intersect_key($this->getRoleNames(), array_filter($this->configuration['allowed_roles'])); + if (empty($role_names)) { + $role_names = ['<' . $this->t('none') . '>']; + } + $summary[] = $this->t('Allowed roles: %roles', ['%roles' => implode(', ', $role_names)]); + + // Autocreate. + if ($this->configuration['autocreate']) { + $summary[] = $this->t('Automatically create roles'); + } + else { + $summary[] = $this->t('Only assign existing roles'); + } + + // Revoke roles. + if ($this->configuration['revoke_roles']) { + $summary[] = $this->t('Revoke roles: yes'); + } + else { + $summary[] = $this->t('Revoke roles: no'); + } + + return $summary; + } + +} diff --git a/src/Plugin/Type/Target/FieldTargetBase.php b/src/Plugin/Type/Target/FieldTargetBase.php index c00d7ec2..331753ee 100644 --- a/src/Plugin/Type/Target/FieldTargetBase.php +++ b/src/Plugin/Type/Target/FieldTargetBase.php @@ -47,6 +47,9 @@ abstract class FieldTargetBase extends TargetBase implements ConfigurableTargetI $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($processor->entityType(), $processor->bundle()); foreach ($field_definitions as $id => $field_definition) { + if (isset($targets[$id])) { + continue; + } if ($id === $processor->bundleKey()) { continue; } diff --git a/tests/modules/feeds_test_alter_source/feeds_test_alter_source.services.yml b/tests/modules/feeds_test_alter_source/feeds_test_alter_source.services.yml index dcc565f8..d6031f8c 100644 --- a/tests/modules/feeds_test_alter_source/feeds_test_alter_source.services.yml +++ b/tests/modules/feeds_test_alter_source/feeds_test_alter_source.services.yml @@ -3,3 +3,8 @@ services: class: Drupal\feeds_test_alter_source\EventSubscriber\CsvFeed tags: - {name: event_subscriber} + + feeds_test_alter_source.user_feed: + class: Drupal\feeds_test_alter_source\EventSubscriber\UserFeed + tags: + - {name: event_subscriber} diff --git a/tests/modules/feeds_test_alter_source/src/EventSubscriber/UserFeed.php b/tests/modules/feeds_test_alter_source/src/EventSubscriber/UserFeed.php new file mode 100644 index 00000000..5ba9be46 --- /dev/null +++ b/tests/modules/feeds_test_alter_source/src/EventSubscriber/UserFeed.php @@ -0,0 +1,46 @@ + [ + ['afterParse', FeedsEvents::AFTER], + ], + ]; + } + + /** + * Acts on parser result. + */ + public function afterParse(ParseEvent $event) { + if ($event->getFeed()->getType()->id() != 'user_import') { + // Not interested in this feed. Abort. + return; + } + + /** @var \Drupal\feeds\Feeds\Item\ItemInterface $item */ + foreach ($event->getParserResult() as $item) { + // Convert roles value to multiple values. + foreach (['role_ids', 'role_labels'] as $source_name) { + $data = $item->get($source_name); + if (!empty($data)) { + $item->set($source_name, explode('|', $data)); + } + } + } + } + +} diff --git a/tests/resources/csv/content-with-config-reference.csv b/tests/resources/csv/content-with-config-reference.csv new file mode 100644 index 00000000..9fc78a83 --- /dev/null +++ b/tests/resources/csv/content-with-config-reference.csv @@ -0,0 +1,3 @@ +"guid","title","type" +1,"Eodem modo typi","test" +2,"Aliquam feugiat diam","test2" diff --git a/tests/resources/csv/users_roles.csv b/tests/resources/csv/users_roles.csv new file mode 100644 index 00000000..0862b625 --- /dev/null +++ b/tests/resources/csv/users_roles.csv @@ -0,0 +1,5 @@ +name,mail,since,password,role_ids,role_labels +Morticia,morticia@example.com,1244347500,mort,editor,"Article Editor" +Fester,fester@example.com,1241865600,fest,manager,"Account Manager" +Gomez,gomez@example.com,1228572000,gome,tester| |manager,"Software Tester| |Account Manager" +Pugsley,pugsley@example.com,1228260225,pugs,, \ No newline at end of file diff --git a/tests/src/Kernel/Feeds/Target/ConfigEntityReferenceTest.php b/tests/src/Kernel/Feeds/Target/ConfigEntityReferenceTest.php new file mode 100644 index 00000000..d0127142 --- /dev/null +++ b/tests/src/Kernel/Feeds/Target/ConfigEntityReferenceTest.php @@ -0,0 +1,189 @@ + 'test', + 'label' => 'Lorem', + 'description' => 'My test description', + ])->save(); + + EntityTestBundle::create([ + 'id' => 'test2', + 'label' => 'Ut wisi', + 'description' => 'My test2 description', + ])->save(); + + // Create a config entity reference field. + $this->createEntityReferenceField('node', 'article', 'field_entity_test_type', 'Type', 'entity_test_bundle'); + } + + /** + * Tests importing config entity references by ID. + */ + public function testImportById() { + // Create a feed type, map to created field. + $feed_type = $this->createFeedTypeForCsv([ + 'guid' => 'guid', + 'title' => 'title', + 'type' => 'type', + ], [ + 'mappings' => array_merge($this->getDefaultMappings(), [ + [ + 'target' => 'field_entity_test_type', + 'map' => ['target_id' => 'type'], + 'settings' => ['reference_by' => 'id'], + ], + ]), + ]); + + // Import. + $feed = $this->createFeed($feed_type->id(), [ + 'source' => $this->resourcesPath() . '/csv/content-with-config-reference.csv', + ]); + $feed->import(); + + // Assert two created nodes. + $this->assertNodeCount(2); + + // Test target id values of these nodes. + $expected_values = [ + 1 => 'test', + 2 => 'test2', + ]; + foreach ($expected_values as $nid => $expected_value) { + $node = Node::load($nid); + $this->assertEquals($expected_value, $node->field_entity_test_type->target_id); + } + } + + /** + * Tests importing config entity references by label. + */ + public function testImportByLabel() { + // Create a feed type, map to created field. + $feed_type = $this->createFeedTypeForCsv([ + 'guid' => 'guid', + 'title' => 'title', + 'alpha' => 'alpha', + ], [ + 'mappings' => array_merge($this->getDefaultMappings(), [ + [ + 'target' => 'field_entity_test_type', + 'map' => ['target_id' => 'alpha'], + 'settings' => ['reference_by' => 'label'], + ], + ]), + ]); + + // Import. + $feed = $this->createFeed($feed_type->id(), [ + 'source' => $this->resourcesPath() . '/csv/content.csv', + ]); + $feed->import(); + + // Assert two created nodes. + $this->assertNodeCount(2); + + // Test target id values of these nodes. + $expected_values = [ + 1 => 'test', + 2 => 'test2', + ]; + foreach ($expected_values as $nid => $expected_value) { + $node = Node::load($nid); + $this->assertEquals($expected_value, $node->field_entity_test_type->target_id); + } + } + + /** + * Tests importing config entity references by UUID. + */ + public function testImportByUuid() { + // Because it's unpredictable which uuids a config entity gets, let's add an + // event subscriber that sets these values for the 'type' column. + $this->container->get('event_dispatcher') + ->addListener(FeedsEvents::PARSE, function (ParseEvent $event) { + // Set UUID on items. + $counter = 0; + $config_entities = ['test', 'test2']; + foreach ($event->getParserResult() as $item) { + $uuid = $this->entityTypeManager->getStorage('entity_test_bundle') + ->load($config_entities[$counter]) + ->uuid(); + $item->set('type', $uuid); + $counter++; + } + }, FeedsEvents::AFTER); + + // Create a feed type, map to created field. + $feed_type = $this->createFeedTypeForCsv([ + 'guid' => 'guid', + 'title' => 'title', + 'type' => 'type', + ], [ + 'mappings' => array_merge($this->getDefaultMappings(), [ + [ + 'target' => 'field_entity_test_type', + 'map' => ['target_id' => 'type'], + 'settings' => ['reference_by' => 'uuid'], + ], + ]), + ]); + + // Import. + $feed = $this->createFeed($feed_type->id(), [ + 'source' => $this->resourcesPath() . '/csv/content.csv', + ]); + $feed->import(); + + // Assert two created nodes. + $this->assertNodeCount(2); + + // Test target id values of these nodes. + $expected_values = [ + 1 => 'test', + 2 => 'test2', + ]; + foreach ($expected_values as $nid => $expected_value) { + $node = Node::load($nid); + $this->assertEquals($expected_value, $node->field_entity_test_type->target_id); + } + } + +} diff --git a/tests/src/Kernel/Feeds/Target/UserRoleTest.php b/tests/src/Kernel/Feeds/Target/UserRoleTest.php new file mode 100644 index 00000000..8119e63a --- /dev/null +++ b/tests/src/Kernel/Feeds/Target/UserRoleTest.php @@ -0,0 +1,464 @@ +installEntitySchema('user'); + + // Create feed type. + $this->feedType = $this->createFeedTypeForCsv([ + 'name' => 'name', + 'mail' => 'mail', + 'role_ids' => 'role_ids', + 'role_labels' => 'role_labels', + ], [ + 'id' => 'user_import', + 'processor' => 'entity:user', + 'processor_configuration' => [ + 'update_existing' => ProcessorInterface::UPDATE_EXISTING, + 'authorize' => FALSE, + ], + 'mappings' => [ + [ + 'target' => 'name', + 'map' => ['value' => 'name'], + ], + [ + 'target' => 'mail', + 'map' => ['value' => 'mail'], + 'unique' => ['value' => TRUE], + ], + ], + ]); + + $this->userStorage = $this->container->get('entity_type.manager')->getStorage('user'); + $this->roleStorage = $this->container->get('entity_type.manager')->getStorage('user_role'); + } + + /** + * Asserts that the given user has the given role. + * + * @param \Drupal\user\UserInterface $account + * The account to check for the role. + * @param string $rid + * The expected role ID that the user should have. + * @param string $message + * (optional) Assertion message. + */ + protected function assertHasRole(UserInterface $account, $rid, $message = '') { + $this->assertTrue($account->hasRole($rid), $message); + } + + /** + * Asserts that the given user NOT has the given role. + * + * @param \Drupal\user\UserInterface $account + * The account to check for the role. + * @param string $rid + * The expected role ID that the user should NOT have. + * @param string $message + * (optional) Assertion message. + */ + protected function assertNotHasRole(UserInterface $account, $rid, $message = '') { + $this->assertFalse($account->hasRole($rid), $message); + } + + /** + * Asserts the expected number of roles an user has. + * + * This excludes the authenticated user role. + * + * @param int $expected_number_of_roles + * The expected number of roles. + * @param \Drupal\user\UserInterface $account + * The account to check for the role count. + * @param string $message + * (optional) Assertion message. + */ + protected function assertRoleCount($expected_number_of_roles, UserInterface $account, $message = '') { + $this->assertEquals($expected_number_of_roles, count($account->getRoles(TRUE)), $message); + } + + /** + * Tests mapping to role without automatically creating new roles. + */ + public function testWithoutRoleCreation() { + // Create the manager role. + $this->createRole([], 'manager'); + + // Add mapping to role. + $this->feedType->addMapping([ + 'target' => 'roles', + 'map' => ['target_id' => 'role_ids'], + ]); + $this->feedType->save(); + + // Import. + $feed = $this->createFeed($this->feedType->id(), [ + 'source' => $this->resourcesPath() . '/csv/users_roles.csv', + ]); + $feed->import(); + + // Assert that Morticia did not get any roles. + $account = user_load_by_name('Morticia'); + $this->assertNotHasRole($account, 'editor', 'Morticia does not have the editor role.'); + $this->assertRoleCount(0, $account, 'Morticia has no special roles.'); + + // Assert that Fester got the manager role and one role in total. + $account = user_load_by_name('Fester'); + $this->assertHasRole($account, 'manager', 'Fester has the manager role.'); + $this->assertRoleCount(1, $account, 'Fester has one role.'); + + // Assert that Gomez got the manager role but not the tester role, since + // that role doesn't exist on the system. + $account = user_load_by_name('Gomez'); + $this->assertHasRole($account, 'manager', 'Gomez has the manager role.'); + $this->assertNotHasRole($account, 'tester', 'Gomez does not have the tester role.'); + $this->assertRoleCount(1, $account, 'Gomez has one role.'); + + // Assert that Pugsley has no roles. + $account = user_load_by_name('Pugsley'); + $this->assertRoleCount(0, $account, 'Pugsley has no special roles.'); + + // Assert that only one role exists: + // - manager. + $roles = $this->roleStorage->loadMultiple(); + $this->assertEquals(1, count($roles), 'Only one role exists.'); + } + + /** + * Tests mapping to role with automatically creating new roles. + */ + public function testWithRoleCreation() { + // Create the manager role. + $this->createRole([], 'manager'); + + // Add mapping to role. + $this->feedType->addMapping([ + 'target' => 'roles', + 'map' => ['target_id' => 'role_ids'], + 'settings' => [ + 'autocreate' => TRUE, + ], + ]); + $this->feedType->save(); + + // Import CSV file. + $feed = $this->createFeed($this->feedType->id(), [ + 'source' => $this->resourcesPath() . '/csv/users_roles.csv', + ]); + $feed->import(); + + // Assert that Morticia got the editor role and one role in total. + $account = user_load_by_name('Morticia'); + $this->assertHasRole($account, 'editor', 'Morticia has the editor role.'); + $this->assertRoleCount(1, $account, 'Morticia has one role.'); + + // Assert that Fester got the manager role and one role in total. + $account = user_load_by_name('Fester'); + $this->assertHasRole($account, 'manager', 'Fester has the manager role.'); + $this->assertRoleCount(1, $account, 'Fester has one role.'); + + // Assert that Gomez got the manager, the editor role and two roles in + // total. + $account = user_load_by_name('Gomez'); + $this->assertHasRole($account, 'manager', 'Gomez has the manager role.'); + $this->assertHasRole($account, 'tester', 'Gomez has the tester role.'); + $this->assertRoleCount(2, $account, 'Gomez has two roles.'); + + // Assert that Pugsley has no roles. + $account = user_load_by_name('Pugsley'); + $this->assertRoleCount(0, $account, 'Pugsley has no special roles.'); + + // Assert that three roles exist: + // - manager; + // - editor; + // - tester. + $roles = $this->roleStorage->loadMultiple(); + $this->assertEquals(3, count($roles), 'Three roles exist.'); + } + + /** + * Tests mapping to role by role label. + */ + public function testImportByRoleLabels() { + // Create the manager and tester roles. + $this->createRole([], 'account_manager', 'Account Manager'); + $this->createRole([], 'software_tester', 'Software Tester'); + + // Add mapping to role. + $this->feedType->addMapping([ + 'target' => 'roles', + 'map' => ['target_id' => 'role_labels'], + 'settings' => [ + 'reference_by' => 'label', + ], + ]); + $this->feedType->save(); + + // Import CSV file. + $feed = $this->createFeed($this->feedType->id(), [ + 'source' => $this->resourcesPath() . '/csv/users_roles.csv', + ]); + $feed->import(); + + // Assert that Morticia did not get any roles. + $account = user_load_by_name('Morticia'); + $this->assertNotHasRole($account, 'editor', 'Morticia does not have the editor role.'); + $this->assertRoleCount(0, $account, 'Morticia has no special roles.'); + + // Assert that Fester got the manager role and one roles in total. + $account = user_load_by_name('Fester'); + $this->assertHasRole($account, 'account_manager', 'Fester has the manager role.'); + $this->assertRoleCount(1, $account, 'Fester has one role.'); + + // Assert that Gomez got the manager and tester roles. + $account = user_load_by_name('Gomez'); + $this->assertHasRole($account, 'account_manager', 'Gomez has the manager role.'); + $this->assertHasRole($account, 'software_tester', 'Gomez has the tester role.'); + $this->assertRoleCount(2, $account, 'Gomez has two roles.'); + + // Assert that Pugsley has no roles. + $account = user_load_by_name('Pugsley'); + $this->assertRoleCount(0, $account, 'Pugsley has no special roles.'); + + // Assert that two roles exist: + // - manager; + // - tester. + $roles = $this->roleStorage->loadMultiple(); + $this->assertEquals(2, count($roles), 'Two roles exist.'); + } + + /** + * Tests mapping to role using only allowed roles. + */ + public function testWithAllowedRoles() { + // Create the manager and editor roles. + $this->createRole([], 'manager'); + $this->createRole([], 'editor'); + + // Add mapping to role. The manager role may not be assigned to the user by + // the feed. + $this->feedType->addMapping([ + 'target' => 'roles', + 'map' => ['target_id' => 'role_ids'], + 'settings' => [ + 'allowed_roles' => [ + 'manager' => FALSE, + 'editor' => 'editor', + ], + 'autocreate' => TRUE, + ], + ]); + $this->feedType->save(); + + // Import CSV file. + $feed = $this->createFeed($this->feedType->id(), [ + 'source' => $this->resourcesPath() . '/csv/users_roles.csv', + ]); + $feed->import(); + + // Assert that Morticia got the editor role and one role in total. + $account = user_load_by_name('Morticia'); + $this->assertHasRole($account, 'editor', 'Morticia has the editor role.'); + $this->assertRoleCount(1, $account, 'Morticia has one role.'); + + // Assert that Fester did not got the manager role, because that role was + // not an allowed value. + $account = user_load_by_name('Fester'); + $this->assertNotHasRole($account, 'manager', 'Fester does not have the manager role.'); + $this->assertRoleCount(0, $account, 'Fester has no special roles.'); + + // Assert that Gomez only got the tester role and not the manager role. + $account = user_load_by_name('Gomez'); + $this->assertNotHasRole($account, 'manager', 'Gomez does not have the manager role.'); + $this->assertHasRole($account, 'tester', 'Gomez has the tester role.'); + $this->assertRoleCount(1, $account, 'Gomez has one role.'); + } + + /** + * Tests that roles can be revoked and that only allowed roles are revoked. + */ + public function testRevokeRoles() { + // Create the manager, editor and tester roles. + $this->createRole([], 'manager'); + $this->createRole([], 'editor'); + $this->createRole([], 'tester'); + + // Add mapping to role. The manager role may not be revoked, but the editor + // role may. + $this->feedType->addMapping([ + 'target' => 'roles', + 'map' => ['target_id' => 'role_ids'], + 'settings' => [ + 'allowed_roles' => [ + 'manager' => FALSE, + 'editor' => 'editor', + 'tester' => 'tester', + ], + 'revoke_roles' => TRUE, + ], + ]); + $this->feedType->save(); + + // Create account for Morticia with roles "manager" and "editor". In the + // source only "editor" is specified. Morticia should keep both roles. + $this->userStorage->create([ + 'name' => 'Morticia', + 'mail' => 'morticia@example.com', + 'pass' => 'mort', + 'status' => 1, + 'roles' => [ + 'manager', + 'editor', + ], + ])->save(); + // Create account for Pugsley with roles "manager", "editor" and "tester". + // Pugsley has no roles in the source so should only keep the "manager" + // role. + $this->userStorage->create([ + 'name' => 'Pugsley', + 'mail' => 'pugsley@example.com', + 'pass' => 'pugs', + 'status' => 1, + 'roles' => [ + 'manager', + 'editor', + 'tester', + ], + ])->save(); + // Create account for Gomez and give it the "editor" role. Gomez has roles + // "tester" and "manager" in the source, so it should lose the "editor" role + // and gain the "tester" role. + $this->userStorage->create([ + 'name' => 'Gomez', + 'mail' => 'gomez@example.com', + 'pass' => 'gome', + 'status' => 1, + 'roles' => [ + 'editor', + ], + ])->save(); + + // Import CSV file. + $feed = $this->createFeed($this->feedType->id(), [ + 'source' => $this->resourcesPath() . '/csv/users_roles.csv', + ]); + $feed->import(); + + // Assert that Morticia kept the manager and editor roles. + $account = user_load_by_name('Morticia'); + $this->assertHasRole($account, 'manager', 'Morticia still has the manager role.'); + $this->assertHasRole($account, 'editor', 'Morticia has the editor role.'); + $this->assertRoleCount(2, $account, 'Morticia has two roles.'); + + // Assert that Pugsley only kept the manager role. + $account = user_load_by_name('Pugsley'); + $this->assertHasRole($account, 'manager', 'Pugsley still has the manager role.'); + $this->assertNotHasRole($account, 'editor', 'Pugsley no longer has the editor role.'); + $this->assertNotHasRole($account, 'tester', 'Pugsley no longer has the tester role.'); + $this->assertRoleCount(1, $account, 'Pugsley has one role.'); + + // Assert that Gomez lost the editor role, and gained the tester role. + $account = user_load_by_name('Gomez'); + $this->assertNotHasRole($account, 'editor', 'Gomez no longer has the editor role.'); + $this->assertHasRole($account, 'tester', 'Gomez has the tester role.'); + $this->assertRoleCount(1, $account, 'Gomez has one role.'); + } + + /** + * Tests if no roles are revoked if the option "Revoke roles" is disabled. + */ + public function testNoRevokeRoles() { + // Create the manager and editor roles. + $this->createRole([], 'manager'); + $this->createRole([], 'editor'); + + // Add mapping to role. Set option to not revoke roles. + $this->feedType->addMapping([ + 'target' => 'roles', + 'map' => ['target_id' => 'role_ids'], + 'settings' => [ + 'allowed_roles' => [ + 'manager' => FALSE, + 'editor' => 'editor', + ], + 'revoke_roles' => FALSE, + ], + ]); + $this->feedType->save(); + + // Create account for Pugsley with roles "manager" and "editor". Pugsley has + // no roles, but roles should not be revoked, so Pugsley should keep all + // roles. + $this->userStorage->create([ + 'name' => 'Pugsley', + 'mail' => 'pugsley@example.com', + 'pass' => 'pugs', + 'status' => 1, + 'roles' => [ + 'manager', + 'editor', + ], + ])->save(); + + // Import CSV file. + $feed = $this->createFeed($this->feedType->id(), [ + 'source' => $this->resourcesPath() . '/csv/users_roles.csv', + ]); + $feed->import(); + + // Assert that Pugsley kept all roles. + $account = user_load_by_name('Pugsley'); + $this->assertHasRole($account, 'manager', 'Pugsley still has the manager role.'); + $this->assertHasRole($account, 'editor', 'Pugsley still has the editor role.'); + $this->assertRoleCount(2, $account, 'Pugsley has two roles.'); + } + +} diff --git a/tests/src/Traits/FeedsCommonTrait.php b/tests/src/Traits/FeedsCommonTrait.php index c4d428ff..de64c371 100644 --- a/tests/src/Traits/FeedsCommonTrait.php +++ b/tests/src/Traits/FeedsCommonTrait.php @@ -204,4 +204,12 @@ trait FeedsCommonTrait { } } + /** + * Prints messages useful for debugging. + */ + protected function printMessages() { + $messages = \Drupal::messenger()->all(); + print_r($messages); + } + }