diff --git a/.travis.yml b/.travis.yml index ea2e4b03b..81c01c9c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,11 @@ sudo: false php: - 5.5 - 5.6 + - 7.0 + +env: + - DRUPAL_CORE=8.1.x + - DRUPAL_CORE=8.2.x mysql: database: og @@ -11,23 +16,42 @@ mysql: encoding: utf8 before_script: - # Remember the current rules test directory for later use in the Drupal - # installation. + # Remove Xdebug as we don't need it and it causes "PHP Fatal error: Maximum + # function nesting level of '256' reached." + # We also don't care if that file exists or not on PHP 7. + - phpenv config-rm xdebug.ini || true + + # Remember the current directory for later use in the Drupal installation. - TESTDIR=$(pwd) + # Navigate out of module directory to prevent blown stack by recursive module # lookup. - cd .. # Create database. - mysql -e 'create database og' + + # Export database variable for kernel tests. + - export SIMPLETEST_DB=mysql://root:@127.0.0.1/og + # Download Drupal 8 core. - - git clone --branch 8.0.x --depth 1 http://git.drupal.org/project/drupal.git + - travis_retry git clone --branch $DRUPAL_CORE --depth 1 https://git.drupal.org/project/drupal.git - cd drupal + # Install Composer dependencies. + - composer self-update && composer install + + # Reference OG in the Drupal site. - ln -s $TESTDIR modules/og - # Adding DB so PHPUnit could mock the environment. - - export SIMPLETEST_DB=mysql://root:@127.0.0.1/og; + # Start a web server on port 8888 in the background. + - nohup php -S localhost:8888 > /dev/null 2>&1 & + + # Wait until the web server is responding. + - until curl -s localhost:8888; do true; done > /dev/null + + # Export web server URL for browser tests. + - export SIMPLETEST_BASE_URL=http://localhost:8888 script: # Run the PHPUnit tests which also include the kernel tests. diff --git a/README.md b/README.md index 17dad2105..b1e3a616a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,48 @@ As is the case with Drupal itself, in Organic Groups different permissions can be assigned to different user roles. This allows group members to perform a different set of actions, in different group contexts. +### OG Membership Entity + +The membership entity that connects a group and a user. + +When dealing with non-user entities that are group content, that is content +that is associated with a group, we do it via an entity reference field that +has the default storage. The only information that we hold is that a group +content is referencing a group. + +However, when dealing with the user entity we recognize that we need to +special case it. It won't suffice to just hold the reference between the user +and the group content as it will be laking crucial information such as: the +state of the user's membership in the group (active, pending or blocked), the +time the membership was created, the user's OG role in the group, etc. + +For this meta data we have the fieldable OgMembership entity, that is always +connecting between a user and a group. There cannot be an OgMembership entity +connecting two non-user entities. + +Creating such a relation is done for example in the following way: + +```php + $membership = OgMembership::create(['type' => \Drupal\og\OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser(2) + ->setEntityId(1) + ->setGroupEntityType('node') + ->setFieldName(OgGroupAudienceHelper::DEFAULT_FIELD) + ->save(); +``` + +Notice how the relation of the user to the group also includes the OG +audience field name this association was done by. Like this we are able to +express different membership types such as the default membership that comes +out of the box, or a "premium membership" that can be for example expired +after a certain amount of time (the logic for the expired membership in the +example is out of the scope of OG core). + +Having this field separation is what allows having multiple OG audience +fields attached to the user, where each group they are associated with may be +a result of different membership types. + ## INSTALLATION DRUPAL 8.x Note that the following guide is here to get you started. Names for content types, groups and group content given here are suggestions and are given to diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..a6359acfa --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "drupal/og", + "description": "API to allow associating content with groups.", + "type": "drupal-module", + "license": "GPL-2.0+", + "homepage": "https://drupal.org/project/og", + "support": { + "issues": "https://drupal.org/project/issues/og", + "irc": "irc://irc.freenode.org/drupal-og", + "source": "https://cgit.drupalcode.org/og" + }, + "minimum-stability": "dev" +} diff --git a/config/install/og.settings.yml b/config/install/og.settings.yml index 534051c13..e8f0ae2b2 100644 --- a/config/install/og.settings.yml +++ b/config/install/og.settings.yml @@ -1,5 +1,5 @@ group_manager_full_access: true groups: [] node_access_strict: true -orphans_delete: false -use_queue: false +delete_orphans: false +delete_orphans_plugin_id: simple diff --git a/config/schema/og.schema.yml b/config/schema/og.schema.yml index e8caaff7e..b22ad62c5 100644 --- a/config/schema/og.schema.yml +++ b/config/schema/og.schema.yml @@ -20,6 +20,28 @@ field.field_settings.og_membership_reference: type: boolean label: 'Access Override' +field.storage_settings.og_standard_reference: + type: mapping + label: 'Organic Groups reference field storage settings' + mapping: + target_type: + type: string + label: 'Type of entity to reference' + +field.field_settings.og_standard_reference: + type: mapping + label: 'Organic Groups reference field settings' + mapping: + handler: + type: string + label: 'Reference method' + handler_settings: + type: entity_reference_selection.[%parent.handler] + label: 'Organic Groups reference selection plugin settings' + access_override: + type: boolean + label: 'Access Override' + og.settings: type: config_object label: 'Organic Groups settings' @@ -34,12 +56,12 @@ og.settings: node_access_strict: type: boolean label: 'Strict node access permissions' - orphans_delete: + delete_orphans: type: boolean label: 'Delete orphaned group content when a group is deleted' - use_queue: - type: boolean - label: 'Use queue' + delete_orphans_plugin_id: + type: string + label: 'The method to use when deleting orphaned group content' og.settings.group.*: type: sequence @@ -83,14 +105,34 @@ og.og_role.*: type: string label: 'Group ID' group_type: - type: label + type: string label: 'Group type' group_bundle: - type: label + type: string label: 'Group bundle' + is_admin: + type: boolean + label: 'User is group admin' permissions: type: sequence label: 'Permissions' sequence: type: string label: 'Permission' + role_type: + type: string + label: 'Role type' + +field.widget.settings.og_complex: + type: mapping + label: 'OG Group Audience field widget' + mapping: + match_operator: + type: string + label: 'Autocomplete matching' + size: + type: integer + label: 'Size of textfield' + placeholder: + type: label + label: 'Placeholder' diff --git a/og.install b/og.install index 55c496769..0365819a8 100644 --- a/og.install +++ b/og.install @@ -5,94 +5,20 @@ * Install, update, and uninstall functions for the Organic groups module. */ +/** + * Implements hook_uninstall(). + */ +function og_uninstall() { + \Drupal::queue('og_orphaned_group_content')->deleteQueue(); + \Drupal::queue('og_orphaned_group_content_cron')->deleteQueue(); +} + /** * Implements hook_schema(). */ function og_schema() { $schema = array(); - $schema['og_role_permission'] = array( - 'description' => 'Stores the permissions assigned to user roles per group.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'description' => "The role permission unique identifier.", - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'rid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Foreign Key: {role}.rid.', - ), - 'permission' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'A single permission granted to the role identified by rid.', - ), - 'module' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "The module declaring the permission.", - ), - ), - 'primary key' => array('id'), - 'indexes' => array( - 'permission' => array('permission'), - ), - 'foreign keys' => array( - 'og_role' => array( - 'table' => 'og_role', - 'columns' => array('rid' => 'rid'), - ), - ), - ); - - $schema['og_role'] = array( - 'description' => 'Stores user roles per group.', - 'fields' => array( - 'rid' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary Key: Unique role ID.', - ), - 'gid' => array( - 'description' => "The group's unique ID.", - 'type' => 'int', - 'size' => 'normal', - 'not null' => TRUE, - ), - 'group_type' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "The group's entity type.", - ), - 'group_bundle' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "The group's bundle name.", - ), - 'name' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Unique role name per group.', - ), - ), - 'primary key' => array('rid'), - ); - $schema['og_users_roles'] = array( 'description' => 'Maps users to roles.', 'fields' => array( diff --git a/og.module b/og.module index 8a8cfa72d..e38d8ea0d 100755 --- a/og.module +++ b/og.module @@ -7,15 +7,16 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\og\Entity\OgRoleInterface; +use Drupal\og\Entity\OgMembership; use Drupal\og\Og; use Drupal\og\OgAccess; -use Drupal\og\Entity\OgMembership; +use Drupal\og\OgGroupAudienceHelper; use Drupal\og\OgMembershipInterface; +use Drupal\og\OgRoleInterface; use Drupal\user\EntityOwnerInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldItemListInterface; /** * Group default roles and permissions field. @@ -34,15 +35,39 @@ function og_entity_insert(EntityInterface $entity) { // Subscribe the group manager. $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); $membership - ->setMemberEntityId($entity->getOwnerId()) - ->setMemberEntityType('user') - ->setGroupEntityid($entity->id()) + ->setUser($entity->getOwnerId()) + ->setEntityId($entity->id()) ->setGroupEntityType($entity->getEntityTypeId()) ->setFieldName($membership->getFieldName()) ->save(); } } +/** + * Implements hook_entity_predelete(). + */ +function og_entity_predelete(EntityInterface $entity) { + if (Og::isGroup($entity->getEntityTypeId(), $entity->bundle())) { + // Register orphaned group content for deletion, if this option has been + // enabled. + $config = \Drupal::config('og.settings'); + if ($config->get('delete_orphans')) { + $plugin_id = $config->get('delete_orphans_plugin_id'); + /** @var \Drupal\og\OgDeleteOrphansInterface $plugin */ + $plugin = \Drupal::service('plugin.manager.og.delete_orphans')->createInstance($plugin_id, []); + $plugin->register($entity); + } + + // @todo Delete user roles. + // @see https://github.com/amitaibu/og/issues/175 + // og_delete_user_roles_by_group($entity_type, $entity); + } + if ($entity instanceof \Drupal\user\UserInterface) { + // @todo Delete memberships when deleting users. + // @see https://github.com/amitaibu/og/issues/176 + } +} + /** * Implements hook_entity_field_access(). * @@ -64,7 +89,7 @@ function og_entity_field_access($operation, FieldDefinitionInterface $field_defi return AccessResult::neutral(); } - if (!Og::isGroupAudienceField($field_definition)) { + if (!OgGroupAudienceHelper::isGroupAudienceField($field_definition)) { return AccessResult::neutral(); } @@ -79,7 +104,7 @@ function og_entity_field_access($operation, FieldDefinitionInterface $field_defi * Implements hook_entity_access(). */ function og_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { - // We only care about content entities. + // We only care about content entities that are groups or group content. if (!$entity instanceof ContentEntityInterface) { return AccessResult::neutral(); } @@ -91,21 +116,18 @@ function og_entity_access(EntityInterface $entity, $operation, AccountInterface $entity_type_id = $entity->getEntityTypeId(); $bundle_id = $entity->bundle(); - $access = OgAccess::userAccessEntity('administer group', $entity, $account); - - if ($access->isNeutral()) { - // The node isn't in an OG context, so no need to keep testing. - return $access; - } - else { - // Any and own content. - $access = $access->orIf(OgAccess::userAccessEntity($operation, $entity, $account)); + if (!Og::isGroup($entity_type_id, $bundle_id) && !Og::isGroupContent($entity_type_id, $bundle_id)) { + return AccessResult::neutral(); } - if (!$access->isAllowed() && ($operation === 'update') && Og::isGroup($entity_type_id, $bundle_id)) { - $access = OgAccess::userAccessEntity($operation, $entity, $account); + // If the user has permission to administer all groups, allow access. + if ($account->hasPermission('administer group')) { + return AccessResult::allowed(); } + /** @var \Drupal\Core\Access\AccessResult $access */ + $access = \Drupal::service('og.access')->userAccessEntity($operation, $entity, $account); + if ($access->isAllowed()) { return $access; } @@ -124,11 +146,6 @@ function og_entity_access(EntityInterface $entity, $operation, AccountInterface * Implements hook_entity_create_access(). */ function og_entity_create_access(AccountInterface $account, array $context, $bundle) { - // @todo: Remove when https://www.drupal.org/node/2627852 lands. - if (empty($context['entity_type_id'])) { - return AccessResult::neutral(); - } - $entity_type_id = $context['entity_type_id']; if (!Og::isGroupContent($entity_type_id, $bundle)) { @@ -150,26 +167,25 @@ function og_entity_create_access(AccountInterface $account, array $context, $bun } } - - - // We can't check if user has create permissions, as there is no group // context. However, we can check if there are any groups the user will be - // able to select, and if not, we don't allow access. + // able to select, and if not, we don't allow access but if there are, + // AccessResult::neutral() will be returned in order to not override other + // access results. // @see \Drupal\og\Plugin\EntityReferenceSelection\OgSelection::buildEntityQuery() $required = FALSE; $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle); foreach ($field_definitions as $field_name => $field_definition) { /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ - if (!Og::isGroupAudienceField($field_definition)) { + if (!OgGroupAudienceHelper::isGroupAudienceField($field_definition)) { continue; } $handler = Og::getSelectionHandler($field_definition); if ($handler->getReferenceableEntities()) { - return Accessresult::allowed(); + return Accessresult::neutral(); } // Allow users to create content outside of groups, if none of the @@ -202,40 +218,6 @@ function _og_modules_uninstalled($modules) { og_permissions_delete_by_module($modules); } -/** - * Implements hook_og_permission(). - */ -function _og_og_permission() { - // Generate standard node permissions for all applicable node types. - $perms = array(); - - $perms['update group'] = array( - 'title' => t('Edit group'), - 'description' => t('Edit the group. Note: This permission controls only node entity type groups.'), - 'default role' => array(OgRoleInterface::ADMINISTRATOR), - ); - $perms['administer group'] = array( - 'title' => t('Administer group'), - 'description' => t('Manage group members and content in the group.'), - 'default role' => array(OgRoleInterface::ADMINISTRATOR), - 'restrict access' => TRUE, - ); - - foreach (node_permissions_get_configured_types() as $type) { - $perms = array_merge($perms, og_list_permissions($type)); - } - - return $perms; -} - - -/** - * Implements hook_og_default_roles(). - */ -function _og_og_default_roles() { - return array(OgRoleInterface::ADMINISTRATOR); -} - /** * Implements hook_field_create_instance(). * @@ -257,7 +239,7 @@ function _og_field_create_instance($instance) { // We add a different field, so each field can be set differently. $entity_type = $instance['entity_type']; $bundle = $instance['bundle']; - foreach (array_keys(Og::getAllGroupAudienceFields('user', 'user')) as $field_name) { + foreach (array_keys(OgGroupAudienceHelper::getAllGroupAudienceFields('user', 'user')) as $field_name) { $field = field_info_field($field_name); if ($field['settings']['target_type'] == $entity_type && empty($field['settings']['handler_settings']['target_bundles'])) { @@ -320,7 +302,7 @@ function _og_field_delete_instance($instance) { function _og_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) { list(,, $bundle) = entity_extract_ids($entity_type, $entity); - if (Og::getAllGroupAudienceFields($entity_type, $bundle)) { + if (OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle)) { $form['#validate'][] = 'og_form_group_reference_validate'; } @@ -368,7 +350,7 @@ function _og_form_group_reference_validate($form, &$form_state) { return; } - foreach (array_keys(Og::getAllGroupAudienceFields($entity_type, $bundle)) as $field_name) { + foreach (array_keys(OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle)) as $field_name) { // If there is at least one group selected, return. if (!empty($form_state['values'][$field_name][LANGUAGE_NONE])) { return; @@ -379,6 +361,25 @@ function _og_form_group_reference_validate($form, &$form_state) { form_set_error('og', t('You must select one or more groups for this content.')); } + +/** + * Implements hook_field_formatter_info_alter() + * + * Allow OG audience fields to have entity reference formatters. + */ +function og_field_formatter_info_alter(array &$info) { + foreach (array_keys($info) as $key) { + if (!in_array('entity_reference', $info[$key]['field_types'])) { + // Not an entity reference formatter. + continue; + } + + $info[$key]['field_types'][] = OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE; + $info[$key]['field_types'][] = OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE; + } +} + + /** * Validate handler; Make sure a group can be created. * @@ -509,7 +510,7 @@ function __og_update_entity_fields($entity_type, $entity) { } $wrapper = entity_metadata_wrapper($entity_type, $entity); - foreach (Og::getAllGroupAudienceFields($entity_type, $bundle) as $field_name => $label) { + foreach (OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle) as $field_name => $label) { $field = field_info_field($field_name); $gids = array(); if ($field['cardinality'] == 1) { @@ -529,24 +530,6 @@ function __og_update_entity_fields($entity_type, $entity) { } } -/** - * Implements hook_entity_delete(). - */ -function _og_entity_delete($entity, $entity_type) { - list($id, , $bundle) = entity_extract_ids($entity_type, $entity); - if (og_is_group($entity_type, $entity)) { - og_delete_user_roles_by_group($entity_type, $entity); - og_membership_delete_by_group($entity_type, $entity); - } - if (og_is_group_content_type($entity_type, $bundle)) { - // As the field attachers are called after hook_entity_presave() we - // can't delete the OG memberships here. So we just mark the entity - // as being deleted, and we will do the actual delete in - // OgBehaviorHandler::delete(). - $entity->delete_og_membership = TRUE; - } -} - /** * Implements hook_og_membership_insert(). */ @@ -1169,57 +1152,6 @@ function __og_orphans_move($ids, $group_type, $gid) { } } -/** - * Register memberships for deletion. - * - * if the property "skip_og_membership_delete_by_group" exists on the - * entity, this function _will return early, and allow other implementing - * modules to deal with the deletion logic. - * - * @param $entity_type - * The group type. - * @param $entity - * The group entity object. - */ -function _og_membership_delete_by_group($entity_type, $entity) { - if (!empty($entity->skip_og_membership_delete_by_group)) { - return; - } - - list($gid) = entity_extract_ids($entity_type, $entity); - $query = new EntityFieldQuery(); - $result = $query - ->entityCondition('entity_type', 'og_membership') - ->propertyCondition('group_type', $entity_type, '=') - ->propertyCondition('gid', $gid, '=') - ->execute(); - - if (empty($result['og_membership'])) { - return; - } - - if (\Drupal::config('og.settings')->get('use_queue')) { - $queue = DrupalQueue::get('og_membership_orphans'); - // Add item to the queue. - $data = array( - 'group_type' => $entity_type, - 'gid' => $gid, - // Allow implementing modules to determine the disposition (e.g. delete - // orphan group content). - 'orphans' => array( - 'delete' => isset($entity->og_orphans['delete']) ? $entity->og_orphans['delete'] : \Drupal::config('og.settings')->get('orphans_delete'), - 'move' => isset($entity->og_orphans['move']) ? $entity->og_orphans['move'] : array(), - ), - ); - - // Exit now, as the task will be processed via queue. - return $queue->createItem($data); - } - - // No scalable solution was chosen, so just delete OG memberships. - og_membership_delete_multiple(array_keys($result['og_membership'])); -} - /** * Label callback; Return the label of OG membership entity. */ @@ -1692,7 +1624,7 @@ function _og_get_group_type($entity_type, $bundle_name, $type = 'group') { return (bool)field_info_instance($entity_type, OG_GROUP_FIELD, $bundle_name); } elseif ($type == 'group content') { - return (bool)Og::getAllGroupAudienceFields($entity_type, $bundle_name); + return (bool)OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle_name); } } @@ -2613,7 +2545,7 @@ function _og_get_groups_by_user($account = NULL, $group_type = NULL) { $account = $user; } - if (!Og::getAllGroupAudienceFields('user', 'user')) { + if (!OgGroupAudienceHelper::getAllGroupAudienceFields('user', 'user')) { // User entity doesn't have group audience fields. return; } diff --git a/og.services.yml b/og.services.yml index 6a6360d00..92db7870a 100644 --- a/og.services.yml +++ b/og.services.yml @@ -1,10 +1,24 @@ services: - plugin.manager.og.fields: - class: Drupal\og\OgFieldsPluginManager - parent: default_plugin_manager + og.access: + class: Drupal\og\OgAccess + arguments: ['@config.factory', '@current_user', '@module_handler'] + og.event_subscriber: + class: Drupal\og\EventSubscriber\OgEventSubscriber + arguments: ['@og.permission_manager'] + tags: + - { name: 'event_subscriber' } og.group.manager: class: Drupal\og\GroupManager - arguments: ['@config.factory'] + arguments: ['@config.factory', '@entity_type.manager', '@entity_type.bundle.info', '@event_dispatcher', '@state'] og.permissions: class: Drupal\og\OgPermissionHandler arguments: ['@module_handler', '@string_translation', '@controller_resolver'] + og.permission_manager: + class: Drupal\og\PermissionManager + arguments: ['@og.group.manager', '@entity_type.manager', '@entity_type.bundle.info'] + plugin.manager.og.delete_orphans: + class: Drupal\og\OgDeleteOrphansPluginManager + parent: default_plugin_manager + plugin.manager.og.fields: + class: Drupal\og\OgFieldsPluginManager + parent: default_plugin_manager diff --git a/og.views.inc b/og.views.inc new file mode 100644 index 000000000..6a5cc4dab --- /dev/null +++ b/og.views.inc @@ -0,0 +1,95 @@ + 'uid', + 'base' => 'og_membership', + 'base field' => 'uid', + 'label' => t('OG Membership'), + 'title' => t('OG Membership'), + 'id' => 'standard', + ]; +} + +/** + * Implements hook_field_views_data(). + * + * This is an almost verbatim copy of core_field_views_data() except for the + * field type check. + */ +function og_field_views_data(FieldStorageConfigInterface $field_storage) { + $data = views_field_default_views_data($field_storage); + + // This is the same as entity reference integration as the OG standard + // reference item is no different really. + switch ($field_storage->getType()) { + case 'og_standard_reference': + $entity_manager = \Drupal::entityManager(); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); + + foreach ($data as $table_name => $table_data) { + // Add a relationship to the target entity type. + $target_entity_type_id = $field_storage->getSetting('target_type'); + $target_entity_type = $entity_manager->getDefinition($target_entity_type_id); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $entity_type = $entity_manager->getDefinition($entity_type_id); + $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable(); + $field_name = $field_storage->getName(); + + // Provide a relationship for the entity type with the entity reference + // field. + $args = [ + '@label' => $target_entity_type->getLabel(), + '@field_name' => $field_name, + ]; + $data[$table_name][$field_name]['relationship'] = array( + 'title' => t('@label referenced from @field_name', $args), + 'label' => t('@field_name: @label', $args), + 'group' => $entity_type->getLabel(), + 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $field_storage->getBundles())]), + 'id' => 'standard', + 'base' => $target_base_table, + 'entity type' => $target_entity_type_id, + 'base field' => $target_entity_type->getKey('id'), + 'relationship field' => $field_name . '_target_id', + ); + + // Provide a reverse relationship for the entity type that is referenced by + // the field. + $args['@entity'] = $entity_type->getLabel(); + $args['@label'] = $target_entity_type->getLowercaseLabel(); + $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; + $data[$target_base_table][$pseudo_field_name]['relationship'] = [ + 'title' => t('@entity using @field_name', $args), + 'label' => t('@field_name', ['@field_name' => $field_name]), + 'group' => $target_entity_type->getLabel(), + 'help' => t('Relate each @entity with a @field_name set to the @label.', $args), + 'id' => 'entity_reverse', + 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), + 'entity_type' => $entity_type_id, + 'base field' => $entity_type->getKey('id'), + 'field_name' => $field_name, + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), + 'field field' => $field_name . '_target_id', + 'join_extra' => [ + [ + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ], + ], + ]; + } + break; + } + + return $data; +} diff --git a/og_ui/css/form.css b/og_ui/css/form.css index 5493be753..65a632337 100644 --- a/og_ui/css/form.css +++ b/og_ui/css/form.css @@ -1,3 +1,4 @@ -form .child-item { +form fieldset.child-item, +.form-item-og-orphans-delete-method .description { margin-left: 1.5em; /* LTR */ } diff --git a/og_ui/og_ui.links.menu.yml b/og_ui/og_ui.links.menu.yml index e315cbf0b..41b603b64 100644 --- a/og_ui/og_ui.links.menu.yml +++ b/og_ui/og_ui.links.menu.yml @@ -11,3 +11,19 @@ og_ui.settings: parent: og_ui.admin_index description: 'Administer OG settings.' route_name: og_ui.settings + +og_ui.roles_overview: + title: 'OG roles' + parent: og_ui.admin_index + description: 'Administer OG roles.' + route_name: og_ui.roles_permissions_overview + route_parameters: + type: 'roles' + +og_ui.permissions_overview: + title: 'OG permissions' + parent: og_ui.admin_index + description: 'Administer OG permissions.' + route_name: og_ui.roles_permissions_overview + route_parameters: + type: 'permissions' diff --git a/og_ui/og_ui.module b/og_ui/og_ui.module index 711f96520..056e4e51b 100644 --- a/og_ui/og_ui.module +++ b/og_ui/og_ui.module @@ -9,9 +9,11 @@ use Drupal\Core\Entity\BundleEntityFormBase; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelper; use Drupal\og_ui\BundleFormAlter; +use Drupal\og\OgRoleInterface; /** * Implements hook_form_alter(). @@ -75,18 +77,20 @@ function og_ui_entity_type_save(EntityInterface $entity) { } // Change the field target type and bundle. + if ($field_storage = FieldStorageConfig::loadByName($entity_type_id, OgGroupAudienceHelper::DEFAULT_FIELD)) { + $target_type = $field_storage->getSetting('target_type'); + if (!empty($entity->og_target_type) && $entity->og_target_type !== $target_type) { + // @todo It's probably not possible to change the field storage after the + // field has data. We should disable this option in the UI. + $field_storage->setSetting('target_type', $entity->og_target_type); + $field_storage->save(); + } + } if ($field = FieldConfig::loadByName($entity_type_id, $bundle, OgGroupAudienceHelper::DEFAULT_FIELD)) { $handler_settings = $field->getSetting('handler_settings'); - $save = FALSE; - foreach (['target_type', 'target_bundles'] as $key) { - $entity_key = 'og_' . $key; - if (!isset($handler_settings[$key]) || $entity->$entity_key != $handler_settings[$key]) { - $handler_settings[$key] = $entity->$entity_key; - $field->setSetting('handler_settings', $handler_settings); - $save = TRUE; - } - } - if ($save) { + if (!isset($handler_settings['target_bundles']) || $entity->og_target_bundles != $handler_settings['target_bundles']) { + $handler_settings['target_bundles'] = $entity->og_target_bundles; + $field->setSetting('handler_settings', $handler_settings); $field->save(); } } diff --git a/og_ui/og_ui.routing.yml b/og_ui/og_ui.routing.yml index 81d533fc9..04c448b52 100644 --- a/og_ui/og_ui.routing.yml +++ b/og_ui/og_ui.routing.yml @@ -15,3 +15,28 @@ og_ui.settings: _title: 'OG settings' requirements: _permission: 'administer group' + +og_ui.roles_permissions_overview: + path: 'admin/config/group/{type}' + defaults: + _controller: '\Drupal\og_ui\Controller\OgUiController::rolesPermissionsOverviewPage' + _title_callback: '\Drupal\og_ui\Controller\OgUiController::rolesPermissionsOverviewTitleCallback' + requirements: + _permission: 'administer group' + type: '^(roles|permissions)$' + +og_ui.roles_form: + path: 'admin/config/group/roles/{entity_type}/{bundle}' + defaults: + _form: '\Drupal\og_ui\Form\OgRolesForm' + _title: '@todo - create title callback' + requirements: + _permission: 'administer group' + +og_ui.permissions_form: + path: 'admin/config/group/permissions/{entity_type}/{bundle}' + defaults: + _form: '\Drupal\og_ui\Form\OgPermissionsForm' + _title: '@todo - create title callback' + requirements: + _permission: 'administer group' diff --git a/og_ui/og_ui.services.yml b/og_ui/og_ui.services.yml new file mode 100644 index 000000000..3aed6aa14 --- /dev/null +++ b/og_ui/og_ui.services.yml @@ -0,0 +1,5 @@ +services: + og_ui.event_subscriber: + class: Drupal\og_ui\EventSubscriber\OgUiEventSubscriber + tags: + - { name: 'event_subscriber' } diff --git a/og_ui/src/BundleFormAlter.php b/og_ui/src/BundleFormAlter.php index e2a85ffe1..a5e205472 100644 --- a/og_ui/src/BundleFormAlter.php +++ b/og_ui/src/BundleFormAlter.php @@ -63,8 +63,8 @@ public function formAlter(array &$form, FormStateInterface $form_state) { /** * AJAX callback displaying the target bundles select box. */ - public function ajaxCallback(array $form, array &$form_state) { - return $form['og']['target_bundles']; + public function ajaxCallback(array $form, FormStateInterface $form_state) { + return $form['og']['og_target_bundles']; } /** @@ -73,7 +73,7 @@ public function ajaxCallback(array $form, array &$form_state) { * @param array $form * @param $form_state */ - protected function prepare(array &$form, $form_state) { + protected function prepare(array &$form, FormStateInterface $form_state) { // Example: article. $this->bundle = $this->entity->id(); // Example: Article. @@ -82,115 +82,125 @@ protected function prepare(array &$form, $form_state) { // Example: node. $this->entityTypeId = $this->definition->getBundleOf(); - $form['og'] = array( + $form['og'] = [ '#type' => 'details', '#title' => t('Organic groups'), '#collapsible' => TRUE, '#group' => 'additional_settings', '#description' => t('This bundle may serve as a group, may belong to a group, or may not participate in OG at all.'), - ); + ]; } /** * Adds the "is group?" checkbox. */ - protected function addGroupType(array &$form, $form_state) { - $form['og']['og_is_group'] = array( + protected function addGroupType(array &$form, FormStateInterface $form_state) { + if ($this->entity->isNew()) { + $description = t('Every entity in this bundle is a group which can contain entities and can have members.'); + } + else { + $description = t('Every "%bundle" is a group which can contain entities and can have members.', [ + '%bundle' => Unicode::lcfirst($this->bundleLabel), + ]); + } + $form['og']['og_is_group'] = [ '#type' => 'checkbox', '#title' => t('Group'), '#default_value' => Og::isGroup($this->entityTypeId, $this->bundle), - '#description' => t('Every "%bundle" is a group which can contain entities and can have members.', [ - '%bundle' => Unicode::lcfirst($this->bundleLabel), - ]), - ); + '#description' => $description, + ]; } /** * Adds the "is group content?" checkbox and target settings elements. */ - protected function addGroupContent(array &$form, $form_state) { - $is_group_content = Og::isGroupContent($this->entityTypeId, $this->bundle); - - $target_type_default = FALSE; - $handler_settings = []; - if ($field = FieldConfig::loadByName($this->entityTypeId, $this->bundle, OgGroupAudienceHelper::DEFAULT_FIELD)) { - $handler_settings = $field->getSetting('handler_settings'); - if (isset($handler_settings['target_type'])) { - $target_type_default = $handler_settings['target_type']; - } - } + protected function addGroupContent(array &$form, FormStateInterface $form_state) { + // Get the stored config from the default group audience field if it exists. + $field = FieldConfig::loadByName($this->entityTypeId, $this->bundle, OgGroupAudienceHelper::DEFAULT_FIELD); + $handler_settings = $field ? $field->getSetting('handler_settings') : []; + // Compile a list of group entity types and bundles. $target_types = []; - $bundle_options = []; - $all_group_bundles = Og::groupManager()->getAllGroupBundles(); - foreach ($all_group_bundles as $group_entity_type => $bundles) { - if (!$target_type_default) { - $target_type_default = $group_entity_type; - } - $target_types[$group_entity_type] = \Drupal::entityTypeManager() - ->getDefinition($group_entity_type) - ->getLabel(); - } - - if ($all_group_bundles) { - $bundle_info = \Drupal::service('entity_type.bundle.info') - ->getBundleInfo($target_type_default); - foreach ($all_group_bundles[$target_type_default] as $bundle_name) { - $bundle_options[$bundle_name] = $bundle_info[$bundle_name]['label']; + $target_bundles = []; + foreach (Og::groupManager()->getAllGroupBundles() as $entity_type => $bundles) { + $target_types[$entity_type] = \Drupal::entityTypeManager()->getDefinition($entity_type)->getLabel(); + $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type); + foreach ($bundles as $bundle) { + $target_bundles[$entity_type][$bundle] = $bundle_info[$bundle]['label']; } - $description = ''; - } - else { - $description = t('There are no group bundles defined.'); } - $form['og']['og_group_content_bundle'] = array( + $form['og']['og_group_content_bundle'] = [ '#type' => 'checkbox', '#title' => t('Group content'), - '#default_value' => $is_group_content, - '#description' => $description, - ); + '#default_value' => Og::isGroupContent($this->entityTypeId, $this->bundle), + '#description' => empty($target_bundles) ? t('There are no group bundles defined.') : '', + ]; if ($target_types) { - // Don't show the settings, as there might be multiple OG audience fields - // in the same bundle. - $form['og']['og_target_type'] = array( + // If a group audience field already exists, use its value. Otherwise fall + // back to the first entity type that was returned. + reset($target_types); + $target_type_default = $field && !empty($field->getSetting('target_type')) ? $field->getSetting('target_type') : key($target_types); + + // If the target type was set using AJAX, use that instead of the default. + $ajax_value = $form_state->getValue('og_target_type'); + $target_type_default = $ajax_value ? $ajax_value : $target_type_default; + + $form['og']['og_target_type'] = [ '#type' => 'select', '#title' => t('Target type'), '#options' => $target_types, '#default_value' => $target_type_default, - '#description' => t('The entity type that can be referenced thru this field.'), - '#ajax' => array( + '#description' => t('The entity type that can be referenced through this field.'), + '#ajax' => [ 'callback' => [$this, 'ajaxCallback'], 'wrapper' => 'og-settings-wrapper', - ), - '#states' => array( - 'visible' => array( - ':input[name="og_group_content_bundle"]' => array('checked' => TRUE), - ), - ), - ); + ], + '#states' => [ + 'visible' => [ + ':input[name="og_group_content_bundle"]' => ['checked' => TRUE], + ], + ], + ]; // Get the bundles that are acting as group. - $form['og']['og_target_bundles'] = array( + $form['og']['og_target_bundles'] = [ '#prefix' => '
' . $this->t('There are :count orphans waiting to be deleted.', [ + ':count' => $count, + ]) . '
', + ], + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Start batch deletion'), + '#submit' => ['\Drupal\og\Plugin\OgDeleteOrphans\Batch::batchSubmit'], + '#disabled' => !(bool) $count, + ], + ]; + } + + /** + * Submit handler for ::configurationForm(). + */ + public static function batchSubmit($form, FormStateInterface $form_state) { + $batch = []; + $steps = \Drupal::queue('og_orphaned_group_content')->numberOfItems(); + for ($i = 0; $i < $steps; $i++) { + $batch['operations'][] = ['\Drupal\og\Plugin\OgDeleteOrphans\Batch::step', []]; + } + batch_set($batch); + } + + /** + * Batch step definition callback to process one queue item. + */ + public static function step($context) { + if (!empty($context['interrupted'])) { + return; + } + \Drupal::getContainer()->get('plugin.manager.og.delete_orphans')->createInstance('batch', [])->process(); + } + +} diff --git a/src/Plugin/OgDeleteOrphans/Cron.php b/src/Plugin/OgDeleteOrphans/Cron.php new file mode 100644 index 000000000..ab57ae0bb --- /dev/null +++ b/src/Plugin/OgDeleteOrphans/Cron.php @@ -0,0 +1,49 @@ +deleteOrphan($data['type'], $data['id']); + } + + /** + * {@inheritdoc} + */ + protected function getQueue() { + // By design, every QueueWorker is executed on every cron run and will + // start processing its designated queue. To make sure that our DeleteOrphan + // queue worker will not start processing orphans that have been registered + // by another plugin (e.g. the Batch plugin) we are using a dedicated queue. + return $this->queueFactory->get('og_orphaned_group_content_cron', TRUE); + } + +} diff --git a/src/Plugin/OgDeleteOrphans/Simple.php b/src/Plugin/OgDeleteOrphans/Simple.php new file mode 100644 index 000000000..ac3a6da67 --- /dev/null +++ b/src/Plugin/OgDeleteOrphans/Simple.php @@ -0,0 +1,40 @@ +process(); + } + + /** + * {@inheritdoc} + */ + public function process() { + $queue = $this->getQueue(); + while ($item = $queue->claimItem()) { + $data = $item->data; + $this->deleteOrphan($data['type'], $data['id']); + $queue->deleteItem($item); + } + } + +} diff --git a/src/Plugin/OgFields/AccessField.php b/src/Plugin/OgFields/AccessField.php index 1ba5f08a7..ce577eca7 100644 --- a/src/Plugin/OgFields/AccessField.php +++ b/src/Plugin/OgFields/AccessField.php @@ -25,8 +25,8 @@ class AccessField extends OgFieldBase implements OgFieldsInterface { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldStorageBaseDefinition(array $values = []) { + $values += [ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'settings' => [ 'allowed_values' => [ @@ -38,14 +38,14 @@ public function getFieldStorageConfigBaseDefinition(array $values = array()) { 'type' => 'list_integer', ]; - return parent::getFieldStorageConfigBaseDefinition($values); + return parent::getFieldStorageBaseDefinition($values); } /** * {@inheritdoc} */ - public function getFieldConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldBaseDefinition(array $values = []) { + $values += [ 'default_value' => [0 => ['value' => 0]], 'description' => $this->t('Determine if group should use default roles and permissions.'), 'display_label' => TRUE, @@ -53,32 +53,31 @@ public function getFieldConfigBaseDefinition(array $values = array()) { 'required' => TRUE, ]; - return parent::getFieldConfigBaseDefinition($values); + return parent::getFieldBaseDefinition($values); } /** * {@inheritdoc} */ - public function widgetDefinition() { - return [ + public function getFormDisplayDefinition(array $values = []) { + $values += [ 'type' => 'options_select', 'settings' => [], ]; + + + return $values; } /** * {@inheritdoc} */ - public function viewModesDefinition() { - return [ - 'default' => [ - 'type' => 'list_default', - 'label' => 'above', - ], - 'teaser' => [ - 'type' => 'list_default', - 'label' => 'above', - ], + public function getViewDisplayDefinition(array $values = []) { + $values += [ + 'type' => 'list_default', + 'label' => 'above', ]; + + return $values; } } diff --git a/src/Plugin/OgFields/AudienceField.php b/src/Plugin/OgFields/AudienceField.php index 8f31bdf44..c300f36ac 100644 --- a/src/Plugin/OgFields/AudienceField.php +++ b/src/Plugin/OgFields/AudienceField.php @@ -27,70 +27,66 @@ class AudienceField extends OgFieldBase implements OgFieldsInterface { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldStorageBaseDefinition(array $values = array()) { + $values += [ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, - 'custom_storage' => TRUE, + 'custom_storage' => $this->getEntityType() == 'user', 'settings' => [ - 'handler' => 'og', - 'handler_settings' => [ - 'target_bundles' => [], - 'membership_type' => OgMembershipInterface::TYPE_DEFAULT, - ], 'target_type' => $this->getEntityType(), ], - 'type' => 'og_membership_reference', + 'type' => $this->getEntityType() == 'user' ? OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE : OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE, ]; - return parent::getFieldStorageConfigBaseDefinition($values); + return parent::getFieldStorageBaseDefinition($values); } /** * {@inheritdoc} */ - public function getFieldConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldBaseDefinition(array $values = array()) { + $values += [ 'description' => $this->t('OG group audience reference field.'), 'display_label' => TRUE, 'label' => $this->t('Groups audience'), + 'settings' => [ + 'handler' => 'og', + 'handler_settings' => [], + ], ]; - return parent::getFieldConfigBaseDefinition($values); + return parent::getFieldBaseDefinition($values); } /** * {@inheritdoc} */ - public function widgetDefinition(array $widget = []) { - // Keep this until og_complex widget is back. - return [ + public function getFormDisplayDefinition(array $values = []) { + $values += [ 'type' => 'og_complex', 'settings' => [ 'match_operator' => 'CONTAINS', + 'size' => 60, + 'placeholder' => '', ], ]; + + + return $values; } /** * {@inheritdoc} */ - public function viewModesDefinition(array $view_mode = []) { - return [ - 'default' => [ - 'label' => 'above', - 'type' => 'entity_reference_label', - 'settings' => [ - 'link' => TRUE, - ] - ], - 'teaser' => [ - 'label' => 'above', - 'type' => 'entity_reference_label', - 'settings' => [ - 'link' => TRUE, - ], - ], + public function getViewDisplayDefinition(array $values = []) { + $values += [ + 'label' => 'above', + 'type' => 'entity_reference_label', + 'settings' => [ + 'link' => TRUE, + ] ]; + + return $values; } } diff --git a/src/Plugin/QueueWorker/DeleteOrphan.php b/src/Plugin/QueueWorker/DeleteOrphan.php new file mode 100644 index 000000000..e0779243a --- /dev/null +++ b/src/Plugin/QueueWorker/DeleteOrphan.php @@ -0,0 +1,64 @@ +ogDeleteOrphansPluginManager = $og_delete_orphans_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.og.delete_orphans') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + $this->ogDeleteOrphansPluginManager->createInstance('cron', [])->processItem($data); + } + +} diff --git a/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php index d7a8a3c84..f3d93da6a 100644 --- a/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php @@ -28,7 +28,7 @@ public function validate($value, Constraint $constraint) { } $entity = \Drupal::entityTypeManager() - ->getStorage($value->getFieldDefinition()->getTargetEntityTypeId()) + ->getStorage($value->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type')) ->load($value->get('target_id')->getValue()); if (!$entity) { diff --git a/tests/modules/og_standard_reference_test_views/og_standard_reference_test_views.info.yml b/tests/modules/og_standard_reference_test_views/og_standard_reference_test_views.info.yml new file mode 100644 index 000000000..b0c11c4cd --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/og_standard_reference_test_views.info.yml @@ -0,0 +1,8 @@ +name: 'OG standard reference test views' +type: module +description: 'Provides default views for views OG standard reference tests.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - views diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_mul_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_mul_view.yml new file mode 100644 index 000000000..7cd22a910 --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_mul_view.yml @@ -0,0 +1,120 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_entity_test_mul_view +label: test_og_standard_reference_entity_test_mul_view +module: views +description: '' +tag: '' +base_table: entity_test_mul_property_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + relationship: field_data_test + filters: { } + sorts: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + field_data_test: + id: field_data_test + table: entity_test_mul__field_data_test + field: field_data_test + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_view.yml new file mode 100644 index 000000000..034a745af --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_view.yml @@ -0,0 +1,121 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_entity_test_view +label: test_og_standard_reference_entity_test_view +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test_mul + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + relationship: field_test_data + filters: { } + sorts: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + field_test_data: + id: field_test_data + table: entity_test__field_test_data + field: field_test_data + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - entity_test_view_grants + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_mul_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_mul_view.yml new file mode 100644 index 000000000..07fbbf101 --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_mul_view.yml @@ -0,0 +1,130 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_reverse_entity_test_mul_view +label: test_og_standard_reference_reverse_entity_test_mul_view +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + relationship: reverse__entity_test_mul__field_data_test + filters: { } + sorts: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + id_1: + id: id_1 + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: standard + relationship: reverse__entity_test_mul__field_data_test + header: { } + footer: { } + empty: { } + relationships: + reverse__entity_test_mul__field_data_test: + id: reverse__entity_test_mul__field_data_test + table: entity_test + field: reverse__entity_test_mul__field_data_test + entity_type: entity_test + plugin_id: entity_reverse + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - entity_test_view_grants + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_view.yml new file mode 100644 index 000000000..eb01a89e9 --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_view.yml @@ -0,0 +1,129 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_reverse_entity_test_view +label: test_og_standard_reference_reverse_entity_test_view +module: views +description: '' +tag: '' +base_table: entity_test_mul_property_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + relationship: reverse__entity_test__field_test_data + filters: { } + sorts: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: standard + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + relationship: reverse__entity_test__field_test_data + header: { } + footer: { } + empty: { } + relationships: + reverse__entity_test__field_test_data: + id: reverse__entity_test__field_test_data + table: entity_test_mul_property_data + field: reverse__entity_test__field_test_data + entity_type: entity_test_mul + plugin_id: entity_reverse + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php b/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php index 353a2b7c7..23802e3c8 100644 --- a/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php +++ b/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php @@ -19,34 +19,28 @@ class EntityRestrictedField extends OgFieldBase implements OgFieldsInterface { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { + public function getFieldStorageBaseDefinition(array $values = []) { $values = [ // Restrict the allowed entities. 'entity' => ['node'], 'type' => 'list_integer', ]; - return parent::getFieldStorageConfigBaseDefinition($values); + return parent::getFieldStorageBaseDefinition($values); } - /** - * {@inheritdoc} - */ - public function getFieldConfigBaseDefinition(array $values = array()) { - return parent::getFieldConfigBaseDefinition($values); - } /** * {@inheritdoc} */ - public function widgetDefinition() { + public function getFormDisplayDefinition(array $values = []) { return []; } /** * {@inheritdoc} */ - public function viewModesDefinition() { + public function getViewDisplayDefinition(array $values = []) { return []; } } diff --git a/tests/og.test b/tests/og.test index 6400788a3..8bb9ed0b9 100644 --- a/tests/og.test +++ b/tests/og.test @@ -1947,44 +1947,14 @@ class OgDeleteOrphansTestCase extends DrupalWebTestCase { variable_set('og_use_queue', TRUE); } - /** - * Testing two things: - * When deleting a group, the node of the group will be deleted. - * Associated node with the deleted group and another group won't be deleted. - */ - function testDeleteGroup() { - // Creating two groups. - $first_group = $this->drupalCreateNode(array('type' => $this->group_type)); - $second_group = $this->drupalCreateNode(array('type' => $this->group_type)); - - // Create two nodes. - $first_node = $this->drupalCreateNode(array('type' => $this->node_type)); - og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $first_node)); - og_group('node', $second_group, array('entity_type' => 'node', 'entity' => $first_node)); - - $second_node = $this->drupalCreateNode(array('type' => $this->node_type)); - og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $second_node)); - - // Delete the group. - node_delete($first_group->nid); - - // Execute manually the queue worker. - $queue = DrupalQueue::get('og_membership_orphans'); - $item = $queue->claimItem(); - og_membership_orphans_worker($item->data); - - // Load the nodes we used during the test. - $first_node = node_load($first_node->nid); - $second_node = node_load($second_node->nid); - - // Verify the none orphan node wasn't deleted. - $this->assertTrue($first_node, "The second node is realted to another group and deleted."); - // Verify the orphan node deleted. - $this->assertFalse($second_node, "The orphan node deleted."); - } - /** * Testing the moving of the node to another group when deleting a group. + * + * @todo This functionality still needs to be ported to Drupal 8. + * + * @see og_membership_delete_by_group() + * @see og_test_entity_delete() + * @see https://github.com/amitaibu/og/issues/178 */ function testMoveOrphans() { // Creating two groups. diff --git a/tests/src/Functional/OgComplexWidgetTest.php b/tests/src/Functional/OgComplexWidgetTest.php new file mode 100644 index 000000000..c50dc0433 --- /dev/null +++ b/tests/src/Functional/OgComplexWidgetTest.php @@ -0,0 +1,123 @@ + 'group']); + Og::groupManager()->addGroup('block_content', 'group'); + + // Add a group audience field to the "post" node type, turning it into a + // group content type. + $this->createContentType(['type' => 'post']); + $settings = [ + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => 'block_content', + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', 'post', $settings); + } + + /** + * Tests adding groups with the "Groups audience" and "Other Groups" fields. + * + * @dataProvider ogComplexFieldsProvider + */ + function testFields($field, $field_name) { + $admin_user = $this->drupalCreateUser(['administer group', 'access content', 'create post content']); + $group_owner = $this->drupalCreateUser(['access content', 'create post content']); + + // Create a group content type owned by the group owner. + $values = [ + 'type' => 'group', + 'uid' => $group_owner->id(), + ]; + $group = BlockContent::create($values); + $group->save(); + + // Log in as administrator. + $this->drupalLogin($admin_user); + + // Create a new post in the group by using the given field in the UI. + $edit = [ + 'title[0][value]' => "Group owner's post.", + $field_name => "group ({$group->id()})", + ]; + $this->drupalGet('node/add/post'); + $this->submitForm($edit, 'Save'); + $this->assertSession()->statusCodeEquals(200); + + // Retrieve the post that was created from the database. + /** @var QueryInterface $query */ + $query = $this->container->get('entity.query')->get('node'); + $result = $query + ->condition('type', 'post') + ->range(0, 1) + ->sort('nid', 'DESC') + ->execute(); + $post_nid = reset($result); + + /** @var NodeInterface $post */ + $post = Node::load($post_nid); + + // Check that the post references the group correctly. + /** @var OgMembershipReferenceItemList $reference_list */ + $reference_list = $post->get(OgGroupAudienceHelper::DEFAULT_FIELD); + $this->assertEquals(1, $reference_list->count(), "There is 1 reference after adding a group to the '$field' field."); + $this->assertEquals($group->id(), $reference_list->first()->getValue()['target_id'], "The '$field' field references the correct group."); + } + + /** + * Data provider for ::testFields() + * + * @return array + */ + public function ogComplexFieldsProvider() { + return [ + ['Groups audience', 'og_group_ref[0][target_id]'], + ['Other groups', 'other_groups[0][target_id]'], + ]; + } + +} diff --git a/tests/src/Kernel/Access/OgEntityAccessTest.php b/tests/src/Kernel/Access/OgEntityAccessTest.php new file mode 100644 index 000000000..1b4d22f93 --- /dev/null +++ b/tests/src/Kernel/Access/OgEntityAccessTest.php @@ -0,0 +1,264 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test'); + $this->installSchema('system', 'sequences'); + + $this->groupBundle = Unicode::strtolower($this->randomMachineName()); + + + // Create users, and make sure user ID 1 isn't used. + User::create(['name' => $this->randomString()]); + + $group_owner = User::create(['name' => $this->randomString()]); + $group_owner->save(); + + // A group member with the correct role. + $this->user1 = User::create(['name' => $this->randomString()]); + $this->user1->save(); + + // A group member without the correct role. + $this->user2 = User::create(['name' => $this->randomString()]); + $this->user2->save(); + + // A non-member. + $this->user3 = User::create(['name' => $this->randomString()]); + $this->user3->save(); + + // Admin user. + $this->adminUser = User::create(['name' => $this->randomString()]); + $this->adminUser->save(); + + + // Define the group content as group. + Og::groupManager()->addGroup('entity_test', $this->groupBundle); + + // Create a group and associate with user 1. + $this->group1 = EntityTest::create([ + 'type' => $this->groupBundle, + 'name' => $this->randomString(), + 'user_id' => $group_owner->id(), + ]); + $this->group1->save(); + + // Create another group to help test per group/per account permission + // caching. + $this->group2 = EntityTest::create([ + 'type' => $this->groupBundle, + 'name' => $this->randomString(), + 'user_id' => $group_owner->id(), + ]); + $this->group2->save(); + + /** @var OgRole ogRoleWithPermission */ + $this->ogRoleWithPermission = OgRole::create(); + $this->ogRoleWithPermission + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + // Associate an arbitrary permission with the role. + ->grantPermission('some_perm') + ->save(); + + $this->ogRoleWithPermission2 = OgRole::create(); + $this->ogRoleWithPermission2 + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + // Associate an arbitrary permission with the role. + ->grantPermission('some_perm_2') + ->save(); + + /** @var OgRole ogRoleWithoutPermission */ + $this->ogRoleWithoutPermission = OgRole::create(); + $this->ogRoleWithoutPermission + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + ->grantPermission($this->randomMachineName()) + ->save(); + + $this->ogAdminRole = OgRole::create(); + $this->ogAdminRole + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + ->setIsAdmin(TRUE) + ->save(); + + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user1->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogRoleWithPermission->id()) + ->save(); + + // Also create a membership to the other group. From this we can verify that + // permissions are not bled between groups. + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user1->id()) + ->setEntityId($this->group2->id()) + ->setGroupEntityType($this->group2->getEntityTypeId()) + ->addRole($this->ogRoleWithPermission2->id()) + ->save(); + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user2->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogRoleWithoutPermission->id()) + ->save(); + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->adminUser->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogAdminRole->id()) + ->save(); + } + + /** + * Test access to an arbitrary permission. + */ + public function testAccess() { + $og_access = $this->container->get('og.access'); + + // A member user. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user1)->isAllowed()); + // This user should not have access to 'some_perm_2' as that was only + // assigned to group 2. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm_2', $this->user1)->isForbidden()); + + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user1)->isAllowed()); + + // A member user without the correct role. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user2)->isForbidden()); + + // A non-member user. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user3)->isForbidden()); + + // Group admin user should have access regardless. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->adminUser)->isAllowed()); + $this->assertTrue($og_access->userAccess($this->group1, $this->randomMachineName(), $this->adminUser)->isAllowed()); + + // Add membership to user 3. + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user3->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogRoleWithPermission->id()) + ->save(); + + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user3)->isAllowed()); + } + +} diff --git a/tests/src/Kernel/DefaultPermissionEventIntegrationTest.php b/tests/src/Kernel/DefaultPermissionEventIntegrationTest.php new file mode 100644 index 000000000..eecaeff16 --- /dev/null +++ b/tests/src/Kernel/DefaultPermissionEventIntegrationTest.php @@ -0,0 +1,101 @@ +eventDispatcher = $this->container->get('event_dispatcher'); + $this->ogRoleStorage = $this->container->get('entity_type.manager')->getStorage('og_role'); + + // Create a group entity type. Note that since we are using the EntityTest + // entity we don't actually need to create the group bundle. EntityTest does + // not have real bundles, it just pretends it does. + $this->groupBundleId = $this->randomMachineName(); + Og::groupManager()->addGroup('entity_test', $this->groupBundleId); + } + + /** + * Tests that OG correctly provides the group administrator default role. + */ + public function testPermissionEventIntegration() { + // Query the event listener directly to see if the administrator role is + // present. + /** @var DefaultRoleEvent $event */ + $event = $this->eventDispatcher->dispatch(DefaultRoleEventInterface::EVENT_NAME, new DefaultRoleEvent()); + $expected_roles = [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => 'Administrator', + 'role_type' => OgRoleInterface::ROLE_TYPE_STANDARD, + ], + ]; + $this->assertEquals($event->getRoles(), $expected_roles); + + // Check that the per-group-type default roles are populated. + $expected_roles = [ + OgRoleInterface::ANONYMOUS, + OgRoleInterface::AUTHENTICATED, + OgRoleInterface::ADMINISTRATOR, + ]; + $actual_roles = $this->ogRoleStorage->loadByProperties([ + 'group_type' => 'entity_test', + 'group_bundle' => $this->groupBundleId, + ]); + + $this->assertEquals(count($expected_roles), count($actual_roles)); + + foreach ($expected_roles as $expected_role) { + // The role ID consists of the entity type, bundle and role name. + $expected_key = implode('-', [ + 'entity_test', + $this->groupBundleId, + $expected_role, + ]); + $this->assertArrayHasKey($expected_key, $actual_roles); + } + } + +} diff --git a/tests/src/Kernel/Entity/EntityCreateAccessTest.php b/tests/src/Kernel/Entity/EntityCreateAccessTest.php new file mode 100644 index 000000000..8a26eeac6 --- /dev/null +++ b/tests/src/Kernel/Entity/EntityCreateAccessTest.php @@ -0,0 +1,126 @@ +installConfig(['og']); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create a "group" node type and turn it into a group type. + $this->groupType = NodeType::create([ + 'type' => 'group', + 'name' => $this->randomString(), + ]); + $this->groupType->save(); + Og::groupManager()->addGroup('node', 'group'); + + // Add a group audience field to the "post" node type, turning it into a + // group content type. + $this->groupContentType = NodeType::create([ + 'type' => 'post', + 'name' => $this->randomString(), + ]); + $this->groupContentType->save(); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', 'post'); + } + + /** + * Tests that users that can only view cannot access the entity creation form. + */ + function testViewPermissionDoesNotGrantCreateAccess() { + // Create test user. + $user = User::create(['name' => $this->randomString()]); + $user->save(); + + // Create a group. + Node::create([ + 'title' => $this->randomString(), + 'type' => 'group', + 'uid' => $user->id(), + ])->save(); + + // Make sure the anonymous user exists. This normally is created in the + // install hook of the User module, but this doesn't run in a KernelTest. + // @see user_install() + \Drupal::entityTypeManager() + ->getStorage('user') + ->create(['uid' => 0, 'status' => 0, 'name' => '']) + ->save(); + + // Grant the anonymous user permission to view published content. + /** @var Role $role */ + $role = Role::create(['id' => Role::ANONYMOUS_ID, 'label' => 'anonymous user']) + ->grantPermission('access content'); + $role->save(); + + // Verify that the user does not have access to the entity create form of + // the group content type. + /** @var \Drupal\node\Access\NodeAddAccessCheck $node_access_check */ + $node_access_check = $this->container->get('access_check.node.add'); + $result = $node_access_check->access(User::getAnonymousUser(), $this->groupContentType); + $this->assertNotInstanceOf('\Drupal\Core\Access\AccessResultAllowed', $result); + + // Test that the user can access the entity create form when the permission + // to create group content is granted. Note that node access control is + // cached, so we need to reset it when we change permissions. + $this->container->get('entity.manager')->getAccessControlHandler('node')->resetCache(); + $role->grantPermission('create post content')->trustData()->save(); + $result = $node_access_check->access(User::getAnonymousUser(), $this->groupContentType); + $this->assertInstanceOf('\Drupal\Core\Access\AccessResultAllowed', $result); + } + +} diff --git a/tests/src/Kernel/Entity/FieldAccessTest.php b/tests/src/Kernel/Entity/FieldAccessTest.php index aa6a8c3ff..e198751a9 100644 --- a/tests/src/Kernel/Entity/FieldAccessTest.php +++ b/tests/src/Kernel/Entity/FieldAccessTest.php @@ -26,7 +26,7 @@ class FieldAccessTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['system', 'user', 'field', 'entity_reference', 'entity_test', 'og']; + public static $modules = ['system', 'user', 'field', 'entity_test', 'og']; /** * @var string diff --git a/tests/src/Kernel/Entity/FieldCreateTest.php b/tests/src/Kernel/Entity/FieldCreateTest.php index a5297b581..c2afb4c59 100644 --- a/tests/src/Kernel/Entity/FieldCreateTest.php +++ b/tests/src/Kernel/Entity/FieldCreateTest.php @@ -24,7 +24,7 @@ class FieldCreateTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['user', 'field', 'entity_reference', 'node', 'og', 'og_test', 'options', 'system']; + public static $modules = ['user', 'field', 'node', 'og', 'og_test', 'options', 'system']; /** * @var Array diff --git a/tests/src/Kernel/Entity/GetBundleByBundleTest.php b/tests/src/Kernel/Entity/GetBundleByBundleTest.php new file mode 100644 index 000000000..7702d7f4a --- /dev/null +++ b/tests/src/Kernel/Entity/GetBundleByBundleTest.php @@ -0,0 +1,557 @@ +installConfig(['og']); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + $this->groupManager = $this->container->get('og.group.manager'); + + // Create four groups of two different entity types. + for ($i = 0; $i < 2; $i++) { + $bundle = "group_$i"; + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + BlockContentType::create(['id' => $bundle])->save(); + Og::groupManager()->addGroup('block_content', $bundle); + } + } + + /** + * Tests retrieval of bundles that are referenc[ed|ing] bundles. + * + * This tests the retrieval of the relations between groups and group content + * and vice versa. The retrieval of groups that are referenced by group + * content is done by GroupManager::getGroupBundleIdsByGroupContenBundle() + * while GroupManager::getGroupContentBundleIdsByGroupBundle() handles the + * opposite case. + * + * Both methods are tested here in a single test since they are very similar, + * and not having to set up the entire relationship structure twice reduces + * the total test running time. + * + * @param array $relationships + * An array indicating the relationships between groups and group content + * bundles that need to be set up in the test. + * @param array $expected_group_by_group_content + * An array containing the expected results for the call to + * getGroupBundleIdsByGroupContentBundle(). + * @param array $expected_group_content_by_group + * An array containing the expected results for the 4 calls to + * getGroupContentBundleIdsByGroupBundle() that will be made in the test. + * + * @covers ::getGroupBundleIdsByGroupContentBundle + * @covers ::getGroupContentBundleIdsByGroupBundle + * + * @dataProvider getBundleIdsByBundleProvider + */ + public function testGetBundleIdsByBundle(array $relationships, array $expected_group_by_group_content, array $expected_group_content_by_group) { + // Set up the relations as indicated in the test. + foreach ($relationships as $group_content_entity_type_id => $group_content_bundle_ids) { + foreach ($group_content_bundle_ids as $group_content_bundle_id => $group_audience_fields) { + switch ($group_content_entity_type_id) { + case 'node': + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $group_content_bundle_id, + ])->save(); + break; + case 'block_content': + BlockContentType::create(['id' => $group_content_bundle_id])->save(); + break; + } + foreach ($group_audience_fields as $group_audience_field_key => $group_audience_field_data) { + foreach ($group_audience_field_data as $group_entity_type_id => $group_bundle_ids) { + $settings = [ + 'field_name' => 'group_audience_' . $group_audience_field_key, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $group_entity_type_id, + ], + ], + ]; + + if (!empty($group_bundle_ids)) { + $settings['field_config'] = [ + 'settings' => [ + 'handler_settings' => [ + 'target_bundles' => array_combine($group_bundle_ids, $group_bundle_ids), + ], + ], + ]; + } + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, $group_content_entity_type_id, $group_content_bundle_id, $settings); + } + } + } + } + + // Test ::getGroupBundleIdsByGroupContentBundle(). + foreach ($expected_group_by_group_content as $group_content_entity_type_id => $group_content_bundle_ids) { + foreach ($group_content_bundle_ids as $group_content_bundle_id => $expected_result) { + $this->assertEquals($expected_result, $this->groupManager->getGroupBundleIdsByGroupContentBundle($group_content_entity_type_id, $group_content_bundle_id)); + } + } + + // Test ::getGroupContentBundleIdsByGroupBundle(). + foreach (['node', 'block_content'] as $group_entity_type_id) { + for ($i = 0; $i < 2; $i++) { + $group_bundle_id = 'group_' . $i; + + // If the expected value is omitted, we expect an empty array. + $expected_result = !empty($expected_group_content_by_group[$group_entity_type_id][$group_bundle_id]) ? $expected_group_content_by_group[$group_entity_type_id][$group_bundle_id] : []; + + $this->assertEquals($expected_result, $this->groupManager->getGroupContentBundleIdsByGroupBundle($group_entity_type_id, $group_bundle_id)); + } + } + } + + /** + * Provides test data for testGetBundleIdsByBundle(). + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - An array indicating the relationships between groups and group content + * bundles that need to be set up in the test. + * - An array containing the expected results for the call to + * getGroupBundleIdsByGroupContentBundle(). + * - An array containing the expected results for the 4 calls to + * getGroupContentBundleIdsByGroupBundle() that will be made in the test. + * If an empty array is expected to be returned, this result is omitted. + */ + public function getBundleIdsByBundleProvider() { + return [ + // Test the simplest case: a single group content type that references a + // single group type. + [ + // The first parameter sets up the relations between groups and group + // content. + [ + // Creating group content of type 'node'. + 'node' => [ + // The first of which... + 'group_content_0' => [ + // Has a single group audience field, configured to reference + // groups of type 'node', targeting bundle '0'. + ['node' => ['group_0']], + ], + ], + ], + // The second parameter contains the expected result for the call to + // getGroupBundleIdsByGroupContentBundle(). In this case we expect group + // '0' of type 'node' to be referenced. + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + ], + // Finally, the third parameter contains all 4 expected results for the + // call to getGroupContentBundleIdsByGroupBundle(). In this test only + // node 0 should be referenced, all others should be empty. + // Note that if the result is expected to be an empty array it can be + // omitted from this list. In reality all 4 possible permutations will + // always be tested. + [ + // When calling the method with entity type 'node' and bundle '0' we + // expect an array to be returned containing group content of type + // 'node', bundle '0'. + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + // There is no group content referencing group '1', so we expect an + // empty array. This may be omitted. + 'group_1' => [], + ], + 'block_content' => [ + // This may be omitted. + 'group_0' => [], + // This may be omitted. + 'group_1' => [], + ], + ], + ], + + // When the bundles are left empty, all bundles should be referenced. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => []], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group audience fields referencing both group types. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => []], + ['block_content' => ['group_0', 'group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + 'block_content' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + 'block_content' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group audience fields, one referencing node group 0 and + // the other entity test group 1. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ['block_content' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + 'block_content' => ['group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + ], + 'block_content' => [ + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two different group content entity types referencing the + // same group. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ], + ], + 'block_content' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + 'block_content' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => [ + 'node' => ['group_content_0' => 'group_content_0'], + 'block_content' => ['group_content_0' => 'group_content_0'], + ], + ], + ], + ], + + // Test having two identical group audience fields on the same group + // content type. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ['node' => ['group_0']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group audience fields on the same group content type, + // each referencing a different group bundle of the same type. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ['node' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group content types referencing the same group. The + // second group content type also references another group with a second + // group audience field. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ], + 'group_content_1' => [ + ['node' => ['group_0']], + ['node' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + 'group_content_1' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => [ + 'node' => [ + 'group_content_0' => 'group_content_0', + 'group_content_1' => 'group_content_1', + ], + ], + 'group_1' => ['node' => ['group_content_1' => 'group_content_1']], + ], + ], + ], + + // Bananas. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + 0 => ['node' => ['group_0']], + 1 => ['block_content' => ['group_0', 'group_1']], + ], + 'group_content_1' => [ + 2 => ['block_content' => ['group_1']], + ], + ], + 'block_content' => [ + 'group_content_2' => [ + 0 => ['node' => ['group_0']], + 1 => ['node' => ['group_0']], + 2 => ['node' => ['group_1']], + ], + 'group_content_3' => [ + 3 => ['block_content' => ['group_0', 'group_1']], + ], + 'group_content_4' => [ + 4 => ['node' => ['group_0', 'group_1']], + 5 => ['block_content' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + 'block_content' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + 'group_content_1' => [ + 'block_content' => ['group_1' => 'group_1'], + ], + ], + 'block_content' => [ + 'group_content_2' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + 'group_content_3' => [ + 'block_content' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + 'group_content_4' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + 'block_content' => ['group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => [ + 'node' => ['group_content_0' => 'group_content_0'], + 'block_content' => [ + 'group_content_2' => 'group_content_2', + 'group_content_4' => 'group_content_4', + ], + ], + 'group_1' => [ + 'block_content' => [ + 'group_content_2' => 'group_content_2', + 'group_content_4' => 'group_content_4', + ], + ], + ], + 'block_content' => [ + 'group_0' => [ + 'node' => ['group_content_0' => 'group_content_0'], + 'block_content' => ['group_content_3' => 'group_content_3'], + ], + 'group_1' => [ + 'node' => [ + 'group_content_0' => 'group_content_0', + 'group_content_1' => 'group_content_1', + ], + 'block_content' => [ + 'group_content_3' => 'group_content_3', + 'group_content_4' => 'group_content_4', + ], + ], + ], + ], + ], + ]; + } + +} diff --git a/tests/src/Kernel/Entity/GetGroupContentTest.php b/tests/src/Kernel/Entity/GetGroupContentTest.php new file mode 100644 index 000000000..0d2082ff1 --- /dev/null +++ b/tests/src/Kernel/Entity/GetGroupContentTest.php @@ -0,0 +1,292 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface entityTypeManager */ + $this->entityTypeManager = $this->container->get('entity_type.manager'); + + // Create group admin user. + $this->groupAdmin = User::create(['name' => $this->randomString()]); + $this->groupAdmin->save(); + } + + /** + * Test retrieval of group content that references a single group. + */ + public function testBasicGroupReferences() { + $groups = []; + + // Create two groups of different entity types. + $bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $groups['node'] = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $this->groupAdmin->id(), + ]); + $groups['node']->save(); + + // The Entity Test entity doesn't have 'real' bundles, so we don't need to + // create one, we can just add the group to the fake bundle. + $bundle = Unicode::strtolower($this->randomMachineName()); + Og::groupManager()->addGroup('entity_test', $bundle); + + $groups['entity_test'] = EntityTest::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + 'uid' => $this->groupAdmin->id(), + ]); + $groups['entity_test']->save(); + + // Create 4 group content types, two for each entity type referencing each + // group. Create a group content entity for each. + $group_content = []; + foreach (['node', 'entity_test'] as $entity_type) { + foreach (['node', 'entity_test'] as $target_group_type) { + // Create the group content bundle if it's a node. Entity Test doesn't + // have real bundles. + $bundle = Unicode::strtolower($this->randomMachineName()); + if ($entity_type === 'node') { + NodeType::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + ])->save(); + } + + // Create the groups audience field. + $field_name = "og_$target_group_type"; + $settings = [ + 'field_name' => $field_name, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $groups[$target_group_type]->getEntityTypeId(), + ], + ], + 'field_config' => [ + 'settings' => [ + 'handler_settings' => [ + 'target_bundles' => [$groups[$target_group_type]->bundle() => $groups[$target_group_type]->bundle()], + ], + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, $entity_type, $bundle, $settings); + + // Create the group content entity. + $label_field = $entity_type === 'node' ? 'title' : 'name'; + $entity = $this->entityTypeManager->getStorage($entity_type)->create([ + $label_field => $this->randomString(), + 'type' => $bundle, + $field_name => [['target_id' => $groups[$target_group_type]->id()]], + ]); + $entity->save(); + + $group_content[$entity_type][$target_group_type] = $entity; + } + } + + // Check that Og::getGroupContent() returns the correct group content for + // each group. + foreach (['node', 'entity_test'] as $group_type) { + $result = Og::getGroupContentIds($groups[$group_type]); + foreach (['node', 'entity_test'] as $group_content_type) { + $this->assertEquals([$group_content[$group_content_type][$group_type]->id()], $result[$group_content_type], "The correct $group_content_type group content is returned for the $group_type group."); + } + // Test that the correct results are returned when filtering by entity + // type. + foreach (['node', 'entity_test'] as $filter) { + $result = Og::getGroupContentIds($groups[$group_type], [$filter]); + $this->assertEquals(1, count($result), "Only one entity type is returned when getting $group_type results filtered by $group_content_type group content."); + $this->assertEquals([$group_content[$filter][$group_type]->id()], $result[$filter], "The correct result is returned for the $group_type group, filtered by $group_content_type group content."); + } + } + } + + /** + * Test retrieval of group content that references multiple groups. + */ + public function testMultipleGroupReferences() { + $groups = []; + + // Create two groups. + $bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + for ($i = 0; $i < 2; $i++) { + $groups[$i] = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $this->groupAdmin->id(), + ]); + $groups[$i]->save(); + } + + // Create a group content type. + $bundle = Unicode::strtolower($this->randomMachineName()); + + $settings = [ + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => 'node', + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $settings); + + // Create a group content entity that references both groups. + $group_content = $this->entityTypeManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + 'type' => $bundle, + OgGroupAudienceHelper::DEFAULT_FIELD => [ + ['target_id' => $groups[0]->id()], + ['target_id' => $groups[1]->id()], + ], + ]); + $group_content->save(); + + // Check that Og::getGroupContent() returns the group content entity for + // both groups. + $expected = ['entity_test' => [$group_content->id()]]; + foreach ($groups as $key => $groups) { + $result = Og::getGroupContentIds($groups); + $this->assertEquals($expected, $result, "The group content entity is returned for group $key."); + } + } + + /** + * Test retrieval of group content with multiple group audience fields. + */ + public function testMultipleGroupAudienceFields() { + $groups = []; + + // Create two groups of different entity types. + $bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $groups['node'] = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $this->groupAdmin->id() + ]); + $groups['node']->save(); + + // The Entity Test entity doesn't have 'real' bundles, so we don't need to + // create one, we can just add the group to the fake bundle. + $bundle = Unicode::strtolower($this->randomMachineName()); + Og::groupManager()->addGroup('entity_test', $bundle); + + $groups['entity_test'] = EntityTest::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + 'uid' => $this->groupAdmin->id(), + ]); + $groups['entity_test']->save(); + + // Create a group content type with two group audience fields, one for each + // group. + $bundle = Unicode::strtolower($this->randomMachineName()); + foreach (['entity_test', 'node'] as $target_type) { + $settings = [ + 'field_name' => 'group_audience_' . $target_type, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $target_type, + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $settings); + } + + // Create a group content entity that references both groups. + $values = [ + 'name' => $this->randomString(), + 'type' => $bundle, + ]; + foreach (['entity_test', 'node'] as $target_type) { + $values['group_audience_' . $target_type] = [ + ['target_id' => $groups[$target_type]->id()], + ]; + } + + $group_content = $this->entityTypeManager->getStorage('entity_test')->create($values); + $group_content->save(); + + // Check that Og::getGroupContent() returns the group content entity for + // both groups. + $expected = ['entity_test' => [$group_content->id()]]; + foreach ($groups as $key => $groups) { + $result = Og::getGroupContentIds($groups); + $this->assertEquals($expected, $result, "The group content entity is returned for group $key."); + } + } + +} diff --git a/tests/src/Kernel/Entity/GetGroupsTest.php b/tests/src/Kernel/Entity/GetGroupsTest.php new file mode 100644 index 000000000..b56fb8aa0 --- /dev/null +++ b/tests/src/Kernel/Entity/GetGroupsTest.php @@ -0,0 +1,250 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface entityTypeManager */ + $this->entityTypeManager = $this->container->get('entity_type.manager'); + + $this->groups = []; + + // Create group admin user. + $group_admin = User::create(['name' => $this->randomString()]); + $group_admin->save(); + + // Create four groups of two different entity types. + for ($i = 0; $i < 2; $i++) { + $bundle = "node_$i"; + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $group = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $group_admin->id(), + ]); + $group->save(); + $this->groups['node'][] = $group; + + // The Entity Test entity doesn't have 'real' bundles, so we don't need to + // create one, we can just add the group to the fake bundle. + $bundle = "entity_test_$i"; + Og::groupManager()->addGroup('entity_test', $bundle); + + $group = EntityTest::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + 'uid' => $group_admin->id(), + ]); + $group->save(); + $this->groups['entity_test'][] = $group; + } + + // Create a group content type with two group audience fields, one for each + // group. + $bundle = Unicode::strtolower($this->randomMachineName()); + foreach (['entity_test', 'node'] as $target_type) { + $settings = [ + 'field_name' => 'group_audience_' . $target_type, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $target_type, + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $settings); + } + + // Create a group content entity that references all four groups. + $values = [ + 'name' => $this->randomString(), + 'type' => $bundle, + ]; + foreach (['entity_test', 'node'] as $target_type) { + foreach ($this->groups[$target_type] as $group) { + $values['group_audience_' . $target_type][] = [ + 'target_id' => $group->id(), + ]; + } + } + + $this->groupContent = $this->entityTypeManager->getStorage('entity_test')->create($values); + $this->groupContent->save(); + } + + /** + * Tests retrieval of groups IDs that are associated with given group content. + * + * @param string $group_type_id + * Optional group type ID to be passed as an argument to the method under + * test. + * @param string $group_bundle + * Optional group bundle to be passed as an argument to the method under + * test. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getGroupIds + * @dataProvider groupContentProvider + */ + public function testGetGroupIds($group_type_id, $group_bundle, array $expected) { + $result = Og::getGroupIds($this->groupContent, $group_type_id, $group_bundle); + + // Check that the correct number of results is returned. + $this->assertEquals(count($expected, COUNT_RECURSIVE), count($result, COUNT_RECURSIVE)); + + // Check that all expected results are returned. + foreach ($expected as $expected_type => $expected_keys) { + foreach ($expected_keys as $expected_key) { + $this->assertTrue(in_array($this->groups[$expected_type][$expected_key]->id(), $result[$expected_type])); + } + } + } + + /** + * Tests retrieval of groups that are associated with a given group content. + * + * @param string $group_type_id + * Optional group type ID to be passed as an argument to the method under + * test. + * @param string $group_bundle + * Optional group bundle to be passed as an argument to the method under + * test. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getGroups + * @dataProvider groupContentProvider + */ + public function testGetGroups($group_type_id, $group_bundle, array $expected) { + $result = Og::getGroups($this->groupContent, $group_type_id, $group_bundle); + + // Check that the correct number of results is returned. + $this->assertEquals(count($expected, COUNT_RECURSIVE), count($result, COUNT_RECURSIVE)); + + // Check that all expected results are returned. + foreach ($expected as $expected_type => $expected_keys) { + foreach ($expected_keys as $expected_key) { + /** @var \Drupal\Core\Entity\EntityInterface $expected_group */ + $expected_group = $this->groups[$expected_type][$expected_key]; + /** @var \Drupal\Core\Entity\EntityInterface $group */ + foreach ($result[$expected_type] as $key => $group) { + if ($group->getEntityTypeId() === $expected_group->getEntityTypeId() && $group->id() === $expected_group->id()) { + // The expected result was found. Continue the test. + continue 2; + } + } + // The expected result was not found. + $this->fail("The expected group of type $expected_type and key $expected_key is found."); + } + } + } + + /** + * Tests if the number of groups associated with group content is correct. + * + * @param string $group_type_id + * Optional group type ID to be passed as an argument to the method under + * test. + * @param string $group_bundle + * Optional group bundle to be passed as an argument to the method under + * test. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getGroupCount + * @dataProvider groupContentProvider + */ + public function testGetGroupCount($group_type_id, $group_bundle, array $expected) { + $result = Og::getGroupCount($this->groupContent, $group_type_id, $group_bundle); + + // Check that the correct results is returned. + $this->assertEquals(count($expected, COUNT_RECURSIVE) - count($expected), $result); + } + + /** + * Provides test data. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - An optional string indicating the group type ID to be returned. + * - An optional string indicating the group bundle to be returned. + * - An array containing the expected results to be returned. + */ + public function groupContentProvider() { + return [ + [NULL, NULL, ['node' => [0, 1], 'entity_test' => [0, 1]]], + ['node', NULL, ['node' => [0, 1]]], + ['entity_test', NULL, ['entity_test' => [0, 1]]], + ['node', 'node_0', ['node' => [0]]], + ['entity_test', 'entity_test_1', ['entity_test' => [1]]], + ]; + } + +} diff --git a/tests/src/Kernel/Entity/GetEntityGroupsTest.php b/tests/src/Kernel/Entity/GetUserGroupsTest.php similarity index 94% rename from tests/src/Kernel/Entity/GetEntityGroupsTest.php rename to tests/src/Kernel/Entity/GetUserGroupsTest.php index fe7db91cb..2c7736e73 100644 --- a/tests/src/Kernel/Entity/GetEntityGroupsTest.php +++ b/tests/src/Kernel/Entity/GetUserGroupsTest.php @@ -20,7 +20,7 @@ * * @group og */ -class GetEntityGroupsTest extends KernelTestBase { +class GetUserGroupsTest extends KernelTestBase { /** * {@inheritdoc} @@ -115,7 +115,7 @@ protected function setUp() { * Tests group owners have the correct groups. */ public function testOwnerGroupsOnly() { - $actual = Og::getEntityGroups($this->user1); + $actual = Og::getUserGroups($this->user1); $this->assertCount(1, $actual['entity_test']); $this->assertGroupExistsInResults($this->group1, $actual); @@ -124,7 +124,7 @@ public function testOwnerGroupsOnly() { $this->assertTrue(Og::isMember($this->group1, $this->user1)); $this->assertFalse(Og::isMember($this->group1, $this->user2)); - $actual = Og::getEntityGroups($this->user2); + $actual = Og::getUserGroups($this->user2); $this->assertCount(1, $actual['entity_test']); $this->assertGroupExistsInResults($this->group2, $actual); @@ -138,8 +138,8 @@ public function testOwnerGroupsOnly() { * Tests other groups users are added to. */ public function testOtherGroups() { - // Should be a part of no groups. - $this->assertEquals([], Og::getEntityGroups($this->user3)); + // Should not be a part of any groups. + $this->assertEquals([], Og::getUserGroups($this->user3)); $this->assertFalse(Og::isMember($this->group1, $this->user3)); $this->assertFalse(Og::isMember($this->group2, $this->user3)); @@ -150,7 +150,7 @@ public function testOtherGroups() { // Add user to group 1 should now return that group only. $this->createMembership($this->user3, $this->group1); - $actual = Og::getEntityGroups($this->user3); + $actual = Og::getUserGroups($this->user3); $this->assertCount(1, $actual['entity_test']); $this->assertGroupExistsInResults($this->group1, $actual); @@ -163,7 +163,7 @@ public function testOtherGroups() { // Add to group 2 should also return that. $this->createMembership($this->user3, $this->group2); - $actual = Og::getEntityGroups($this->user3); + $actual = Og::getUserGroups($this->user3); $this->assertCount(2, $actual['entity_test']); $this->assertGroupExistsInResults($this->group1, $actual); @@ -229,9 +229,8 @@ public function testIsMemberStates() { */ protected function createMembership($user, $group, $state = OgMembershipInterface::STATE_ACTIVE) { $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]) - ->setMemberEntityId($user->id()) - ->setMemberEntityType('user') - ->setGroupEntityid($group->id()) + ->setUser($user) + ->setEntityId($group->id()) ->setGroupEntityType($group->getEntityTypeId()) ->setState($state); $membership->save(); diff --git a/tests/src/Kernel/Entity/GetUserMembershipsTest.php b/tests/src/Kernel/Entity/GetUserMembershipsTest.php new file mode 100644 index 000000000..9da9dcfd4 --- /dev/null +++ b/tests/src/Kernel/Entity/GetUserMembershipsTest.php @@ -0,0 +1,231 @@ +installConfig(['og']); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create group admin user. + $group_admin = User::create(['name' => $this->randomString()]); + $group_admin->save(); + + // Create two groups. + for ($i = 0; $i < 2; $i++) { + $bundle = "node_$i"; + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $group = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $group_admin->id(), + ]); + $group->save(); + $this->groups[] = $group; + } + + // Create test users with different membership statuses in the two groups. + $matrix = [ + // A user which is an active member of the first group. + [OgMembershipInterface::STATE_ACTIVE, NULL], + + // A user which is a pending member of the second group. + [NULL, OgMembershipInterface::STATE_PENDING], + + // A user which is an active member of both groups. + [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_ACTIVE], + + // A user which is a pending member of the first group and blocked in the + // second group. + [OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], + + // A user which is not subscribed to either of the two groups. + [NULL, NULL], + ]; + + foreach ($matrix as $user_key => $statuses) { + $user = User::create(['name' => $this->randomString()]); + $user->save(); + $this->users[$user_key] = $user; + foreach ($statuses as $group_key => $status) { + $group = $this->groups[$group_key]; + if ($status) { + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($user->id()) + ->setEntityId($group->id()) + ->setGroupEntityType($group->getEntityTypeId()) + ->setState($status) + ->save(); + } + } + } + } + + /** + * Tests retrieval of OG Membership entities associated with a given user. + * + * @param int $index + * The array index in the $this->users array of the user to test. + * @param array $states + * Array with the states to retrieve. + * @param string $field_name + * The field name associated with the group. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getUserMemberships + * @dataProvider membershipDataProvider + */ + public function testGetUserMemberships($index, array $states, $field_name, array $expected) { + $result = Og::getUserMemberships($this->users[$index], $states, $field_name); + + // Check that the correct number of results is returned. + $this->assertEquals(count($expected), count($result)); + + // Inspect the results that were returned. + foreach ($result as $key => $membership) { + // Check that all result items are OgMembership objects. + $this->assertInstanceOf('Drupal\og\OgMembershipInterface', $membership); + // Check that the results are keyed by OgMembership ID. + $this->assertEquals($membership->id(), $key); + } + + // Check that all expected results are returned. + foreach ($expected as $expected_group) { + $expected_id = $this->groups[$expected_group]->id(); + foreach ($result as $membership) { + if ($membership->getEntityId() === $expected_id) { + // Test successful: the expected result was found. + continue 2; + } + } + $this->fail("The expected group with ID $expected_id was not found."); + } + } + + /** + * Provides test data to test retrieval of memberships. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - The key of the user in the $this->users array for which to retrieve + * memberships. + * - An array of membership states to filter on. + * - The field name to filter on. + * - An array containing the expected results to be returned. + */ + public function membershipDataProvider() { + return [ + // The first user is an active member of the first group. + // Query default values. The group should be returned. + [0, [], NULL, [0]], + // Filter by active state. + [0, [OgMembershipInterface::STATE_ACTIVE], NULL, [0]], + // Filter by active + pending state. + [0, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING], NULL, [0]], + // Filter by blocked + pending state. Since the user is active this should + // not return any matches. + [0, [OgMembershipInterface::STATE_BLOCKED, OgMembershipInterface::STATE_PENDING], NULL, []], + // Filter by a non-existing field name. This should not return any + // matches. + [0, [], 'non_existing_field_name', []], + + // The second user is a pending member of the second group. + // Query default values. The group should be returned. + [1, [], NULL, [1]], + // Filter by pending state. + [1, [OgMembershipInterface::STATE_PENDING], NULL, [1]], + // Filter by active state. The user is pending so this should not return + // any matches. + [1, [OgMembershipInterface::STATE_ACTIVE], NULL, []], + + // The third user is an active member of both groups. + // Query default values. Both groups should be returned. + [2, [], NULL, [0, 1]], + // Filter by active state. + [2, [OgMembershipInterface::STATE_ACTIVE], NULL, [0, 1]], + // Filter by blocked state. This should not return any matches. + [2, [OgMembershipInterface::STATE_BLOCKED], NULL, []], + + // The fourth user is a pending member of the first group and blocked in + // the second group. + // Query default values. Both groups should be returned. + [3, [], NULL, [0, 1]], + // Filter by active state. No results should be returned. + [3, [OgMembershipInterface::STATE_ACTIVE], NULL, []], + // Filter by pending state. + [3, [OgMembershipInterface::STATE_PENDING], NULL, [0]], + // Filter by blocked state. + [3, [OgMembershipInterface::STATE_BLOCKED], NULL, [1]], + // Filter by combinations of states. + [3, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING], NULL, [0]], + [3, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], NULL, [0, 1]], + [3, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_BLOCKED], NULL, [1]], + [3, [OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], NULL, [0, 1]], + + // A user which is not subscribed to either of the two groups. + [4, [], NULL, []], + [4, [OgMembershipInterface::STATE_ACTIVE], NULL, []], + [4, [OgMembershipInterface::STATE_BLOCKED], NULL, []], + [4, [OgMembershipInterface::STATE_PENDING], NULL, []], + [4, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], NULL, []], + ]; + } + +} diff --git a/tests/src/Kernel/Entity/GroupAudienceTest.php b/tests/src/Kernel/Entity/GroupAudienceTest.php index 61a331e66..af3d3a97b 100644 --- a/tests/src/Kernel/Entity/GroupAudienceTest.php +++ b/tests/src/Kernel/Entity/GroupAudienceTest.php @@ -1,37 +1,39 @@ bundles[2]; // Test no values returned for a non-group content. - $this->assertEmpty(Og::getAllGroupAudienceFields('entity_test', $bundle)); + $this->assertEmpty(OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle)); // Set bundles as group content. $field_name1 = Unicode::strtolower($this->randomMachineName()); $field_name2 = Unicode::strtolower($this->randomMachineName()); - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name1]); - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name1]); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); - $field_names = Og::getAllGroupAudienceFields('entity_test', $bundle); - $this->assertEquals(array($field_name1, $field_name2), array_keys($field_names)); + $field_names = OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle); + $this->assertEquals([$field_name1, $field_name2], array_keys($field_names)); // Test Og::isGroupContent method, which is just a wrapper around - // Og::getAllGroupAudienceFields. + // OgGroupAudienceHelper::getAllGroupAudienceFields. $this->assertTrue(Og::isGroupContent('entity_test', $bundle)); $bundle = $this->bundles[3]; @@ -107,14 +109,13 @@ public function testGetAllGroupAudienceFieldsFilterGroupType() { ], ], ]; - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); // Add a default field, which will use the "entity_test" as target type. - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); - - $field_names = Og::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test'); - $this->assertEquals(array($field_name2), array_keys($field_names)); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); + $field_names = OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test'); + $this->assertEquals([$field_name2], array_keys($field_names)); } /** @@ -145,14 +146,14 @@ public function testGetAllGroupAudienceFieldsFilterGroupBundle() { ], ], ]; - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); $overrides['field_name'] = $field_name2; $overrides['field_config']['settings']['handler_settings']['target_bundles'] = [$group_bundle2 => $group_bundle2]; - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); - $field_names = Og::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test', $group_bundle1); - $this->assertEquals(array($field_name1), array_keys($field_names)); + $field_names = OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test', $group_bundle1); + $this->assertEquals([$field_name1], array_keys($field_names)); } } diff --git a/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php b/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php index 899c918e2..d38e5aabc 100644 --- a/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php +++ b/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php @@ -10,13 +10,12 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityInterface; use Drupal\entity_test\Entity\EntityTest; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; use Drupal\og\Entity\OgMembership; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelper; use Drupal\og\OgMembershipInterface; +use Drupal\user\Entity\User; /** * Tests OgMembershipReferenceItem and OgMembershipReferenceItemList classes. @@ -46,6 +45,7 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); // Create several bundles. for ($i = 0; $i <= 4; $i++) { @@ -66,7 +66,7 @@ protected function setUp() { } $this->fieldName = strtolower($this->randomMachineName()); - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $this->bundles[2], ['field_name' => $this->fieldName]); + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'user', 'user', ['field_name' => $this->fieldName]); } /** @@ -76,30 +76,33 @@ public function testMembershipSave() { $run_query = function ($id) { return $this->container->get('entity.query')->get('og_membership') ->condition('field_name', $this->fieldName) - ->condition('member_entity_type', 'entity_test') - ->condition('member_entity_id', $id) - ->condition('group_entity_type', 'entity_test') + ->condition('uid', $id) + ->condition('entity_type', 'user') ->condition('state', OgMembershipInterface::STATE_ACTIVE) ->execute(); }; - $entity = EntityTest::create([ + $entity = User::create([ 'type' => $this->bundles[2], + 'name' => $this->randomString(), ]); // Assert no membership for a group membership with no references. $this->assertSame(count($entity->{$this->fieldName}), 0); $entity->save(); $this->assertSame(count($entity->{$this->fieldName}), 0); $this->assertSame($run_query($entity->id()), []); - $member_in_single_grpup = EntityTest::create([ + $member_in_single_grpup = User::create([ 'type' => $this->bundles[2], + 'name' => $this->randomString(), $this->fieldName => [['target_id' => $this->groups[0]->id()]], ]); + // Assert group membership is found before save. $this->assertSame(count($member_in_single_grpup->{$this->fieldName}), 1); $member_in_single_grpup->save(); $this->assertSame(count($member_in_single_grpup->{$this->fieldName}), 1); $this->assertSame(count($run_query($member_in_single_grpup->id())), 1); - $member_in_two_groups = EntityTest::create([ + $member_in_two_groups = User::create([ + 'name' => $this->randomString(), 'type' => $this->bundles[2], $this->fieldName => [ ['target_id' => $this->groups[0]->id()], @@ -121,10 +124,11 @@ public function testMembershipSave() { */ public function testMembershipLoad() { $reload = function (EntityInterface &$entity) { - $entity = \Drupal::entityTypeManager()->getStorage('entity_test')->loadUnchanged($entity->id()); + $entity = \Drupal::entityTypeManager()->getStorage('user')->loadUnchanged($entity->id()); }; - $entity = EntityTest::create([ + $entity = User::create([ 'type' => $this->bundles[2], + 'name' => $this->randomString(), ]); // Assert no membership for a group membership with no references. $this->assertSame(count($entity->{$this->fieldName}), 0); @@ -133,15 +137,14 @@ public function testMembershipLoad() { $membership = OgMembership::create([ 'type' => $this->bundles[0], 'field_name' => $this->fieldName, - 'member_entity_type' => 'entity_test', - 'member_entity_id' => $entity->id(), - 'group_entity_type' => 'entity_test', - 'group_entity_id' => $this->groups[0]->id(), + 'uid' => $entity->id(), + 'entity_type' => 'user', + 'entity_id' => $this->groups[0]->id(), ]); $membership->save(); $reload($entity); // Assert membership is picked up after a load from database. - $this->assertSame(count($entity->{$this->fieldName}), 1); + $this->assertSame(count($entity->{$this->fieldName}->getValue()), 1); } } diff --git a/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php b/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php new file mode 100644 index 000000000..3fbecdedc --- /dev/null +++ b/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php @@ -0,0 +1,127 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('system', 'sequences'); + + $this->groupBundle = $this->randomMachineName(); + + $this->user = User::create(['name' => $this->randomString()]); + $this->user->save(); + + $this->group = Node::create([ + 'title' => $this->randomString(), + 'uid' => $this->user->id(), + 'type' => $this->groupBundle, + ]); + } + + /** + * Testing OG membership role referencing. + */ + public function testRoleCreate() { + // Creating a content editor role. + $content_editor = OgRole::create(); + $content_editor + ->setGroupType('node') + ->setGroupBundle('group') + ->setId('content_editor') + ->setLabel('Content editor') + ->grantPermission('administer group'); + $content_editor->save(); + + // Create a group member role. + $group_member = OgRole::create(); + $group_member + ->setGroupType('node') + ->setGroupBundle('group') + ->setId('group_member') + ->setLabel('Group member'); + $group_member->save(); + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setGroupEntityType('node') + ->setEntityId($this->group->id()) + ->setUser($this->user) + // Assign only the content editor role for now. + ->setRoles([$content_editor->id()]) + ->save(); + + $roles_ids = $membership->getRolesIds(); + $this->assertTrue(in_array($content_editor->id(), $roles_ids), 'The membership has the content editor role.'); + + // Adding another role to the membership. + $membership->addRole($group_member->id()); + $roles_ids = $membership->getRolesIds(); + + $this->assertTrue(in_array($content_editor->id(), $roles_ids), 'The membership has the content editor role.'); + $this->assertTrue(in_array($group_member->id(), $roles_ids), 'The membership has the group member role.'); + + // Remove a role. + $membership->revokeRole($content_editor->id()); + + $roles_ids = $membership->getRolesIds(); + $this->assertFalse(in_array($content_editor->id(), $roles_ids), 'The membership does not have the content editor role after is has been revoked.'); + $this->assertTrue(in_array($group_member->id(), $roles_ids), 'The membership has the group member role.'); + + // Check if the role has permission from the membership. + $this->assertFalse($membership->hasPermission('administer group'), 'The user has permission to administer groups.'); + $membership->addRole($content_editor->id()); + $this->assertTrue($membership->hasPermission('administer group'), 'The user has permission to administer groups.'); + } + +} diff --git a/tests/src/Kernel/Entity/OgMembershipTest.php b/tests/src/Kernel/Entity/OgMembershipTest.php new file mode 100644 index 000000000..6d349067f --- /dev/null +++ b/tests/src/Kernel/Entity/OgMembershipTest.php @@ -0,0 +1,131 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create a bundle and add as a group + $group = EntityTest::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + ]); + + $group->save(); + $this->group = $group; + + // Add that as a group. + Og::groupManager()->addGroup('entity_test', $group->id()); + + // Create test user. + $user = User::create(['name' => $this->randomString()]); + $user->save(); + + $this->user = $user; + } + + /** + * Tests getting and setting users on OgMemberships. + * + * @covers ::getUser + * @covers ::setUser + */ + public function testGetSetUser() { + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user->id()) + ->setEntityId($this->group->id()) + ->setGroupEntityType($this->group->getEntityTypeId()) + ->save(); + + // Check the user is returned. + $this->assertInstanceOf(UserInterface::class, $membership->getUser()); + $this->assertEquals($this->user->id(), $membership->getUser()->id()); + + // And after re-loading. + $membership = Og::membershipStorage()->loadUnchanged($membership->id()); + + $this->assertInstanceOf(UserInterface::class, $membership->getUser()); + $this->assertEquals($this->user->id(), $membership->getUser()->id()); + } + + /** + * Tests exceptions are thrown when trying to save a membership with no, or + * anonymous user. + * + * @covers ::getUser + * @dataProvider providerTestGetSetUserException + * @expectedException \Drupal\Core\Entity\EntityStorageException + */ + public function testGetSetUserException($user_value) { + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($user_value) + ->setEntityId($this->group->id()) + ->setGroupEntityType($this->group->getEntityTypeId()) + ->save(); + } + + /** + * Data provider for testGetSetUserException. + */ + public function providerTestGetSetUserException() { + return [ + [NULL], + [0] + ]; + } + +} diff --git a/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php b/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php new file mode 100644 index 000000000..bde71a4f5 --- /dev/null +++ b/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php @@ -0,0 +1,97 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create several bundles. + for ($i = 0; $i <= 2; $i++) { + $bundle = EntityTest::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + ]); + + $bundle->save(); + $this->bundles[] = $bundle->id(); + } + for ($i = 0 ; $i < 2; $i++) { + $bundle = $this->bundles[$i]; + Og::groupManager()->addGroup('entity_test', $bundle); + $group = EntityTest::create(['type' => $bundle]); + $group->save(); + $this->groups[] = $group; + } + $this->fieldName = strtolower($this->randomMachineName()); + + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $this->bundles[2], ['field_name' => $this->fieldName]); + } + + /** + * Testing referencing of non-user entity to groups. + */ + public function testStandardReference() { + $groups_query = function($gid) { + return $this->container->get('entity.query')->get('entity_test') + ->condition($this->fieldName, $gid) + ->execute(); + }; + + $entity = EntityTest::create([ + 'type' => $this->bundles[2], + 'name' => $this->randomString(), + ]); + $entity->save(); + + $this->assertEmpty($groups_query($this->groups[0]->id())); + + $entity = EntityTest::create([ + 'type' => $this->bundles[2], + 'name' => $this->randomString(), + $this->fieldName => [['target_id' => $this->groups[1]->id()]], + ]); + $entity->save(); + + $this->assertEmpty($groups_query($this->groups[0]->id())); + $this->assertEquals(array_keys($groups_query($this->groups[1]->id())), [$entity->id()]); + } + +} diff --git a/tests/src/Kernel/Entity/ReferenceStringIdTest.php b/tests/src/Kernel/Entity/ReferenceStringIdTest.php new file mode 100644 index 000000000..e08082394 --- /dev/null +++ b/tests/src/Kernel/Entity/ReferenceStringIdTest.php @@ -0,0 +1,133 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test_string_id'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create two bundles, one will serve as group, the other as group content. + for ($i = 0; $i < 2; $i++) { + $bundle = EntityTestStringId::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + 'id' => $this->randomMachineName(), + ]); + $bundle->save(); + $this->bundles[] = $bundle->id(); + } + + // Create a group with a string as an ID. + $group = EntityTestStringId::create([ + 'type' => $this->bundles[0], + 'id' => $this->randomMachineName(), + ]); + $group->save(); + $this->group = $group; + + // Let OG mark the group entity type as a group. + Og::groupManager()->addGroup('entity_test_string_id', $this->bundles[0]); + + // Add a group audience field to the second bundle, this will turn it into a + // group content type. + $this->fieldName = strtolower($this->randomMachineName()); + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test_string_id', $this->bundles[1], [ + 'field_name' => $this->fieldName, + ]); + + // Add a group audience field to the User entity, so that we can test if + // users can become members of the test group. + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'user', 'user', [ + 'field_name' => $this->fieldName, + ]); + } + + /** + * Test if a group that uses a string as ID can be referenced. + */ + public function testReferencingStringIds() { + // Create a group content entity that references the group. + $entity = EntityTestStringId::create([ + 'type' => $this->bundles[1], + 'name' => $this->randomString(), + 'id' => $this->randomMachineName(), + $this->fieldName => [['target_id' => $this->group->id()]], + ]); + $entity->save(); + + // Check that the group content entity is referenced. + $references = $this->container->get('entity.query')->get('entity_test_string_id') + ->condition($this->fieldName, $this->group->id()) + ->execute(); + $this->assertEquals([$entity->id()], array_keys($references), 'The correct group is referenced.'); + + // Create a user and make it a member of the group. + $user = User::create(['name' => $this->randomString()]); + $user->save(); + $membership = OgMembership::create([ + 'uid' => $user->id(), + 'type' => 'user', + 'entity_type' => 'entity_test_string_id', + 'entity_id' => $this->group->id(), + 'field_name' => $this->fieldName, + ]); + $membership->save(); + + // Reload the user and check that its group audience field correctly + // references the entity. + $user = \Drupal::entityTypeManager()->getStorage('user')->loadUnchanged($user->id()); + $this->assertEquals($this->group->id(), $user->{$this->fieldName}->getValue()[0]['target_id']); + } + +} diff --git a/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php b/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php new file mode 100644 index 000000000..669754c76 --- /dev/null +++ b/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php @@ -0,0 +1,234 @@ +installEntitySchema('user'); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('entity_test_mul'); + + // Create reference from entity_test to entity_test_mul. + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', 'entity_test', ['field_name' => 'field_test_data', 'field_storage_config' => ['settings' => ['target_type' => 'entity_test_mul']]]); + + // Create reference from entity_test_mul to entity_test. + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test_mul', 'entity_test_mul', ['field_name' => 'field_data_test', 'field_storage_config' => ['settings' => ['target_type' => 'entity_test']]]); + + ViewTestData::createTestViews(get_class($this), ['og_standard_reference_test_views']); + } + + /** + * Tests using the views relationship. + */ + public function testNoDataTableRelationship() { + + // Create some test entities which link each other. + $referenced_entity = EntityTestMul::create(); + $referenced_entity->save(); + + $entity = EntityTest::create(); + $entity->field_test_data->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_test_data[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + $entity = EntityTest::create(); + $entity->field_test_data->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_test_data[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + Views::viewsData()->clear(); + + // Check the generated views data. + $views_data = Views::viewsData()->get('entity_test__field_test_data'); + $this->assertEqual($views_data['field_test_data']['relationship']['id'], 'standard'); + $this->assertEqual($views_data['field_test_data']['relationship']['base'], 'entity_test_mul_property_data'); + $this->assertEqual($views_data['field_test_data']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['field_test_data']['relationship']['relationship field'], 'field_test_data_target_id'); + $this->assertEqual($views_data['field_test_data']['relationship']['entity type'], 'entity_test_mul'); + + // Check the backwards reference. + $views_data = Views::viewsData()->get('entity_test_mul_property_data'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['id'], 'entity_reverse'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['base'], 'entity_test'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['field table'], 'entity_test__field_test_data'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['field field'], 'field_test_data_target_id'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['field_name'], 'field_test_data'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['entity_type'], 'entity_test'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['join_extra'][0], ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE]); + + // Check an actual test view. + $view = Views::getView('test_og_standard_reference_entity_test_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + // Check that the actual ID of the entity is the expected one. + $this->assertEqual($row->id, $this->entities[$index]->id()); + + // Also check that we have the correct result entity. + $this->assertEqual($row->_entity->id(), $this->entities[$index]->id()); + + // Test the forward relationship. + $this->assertEqual($row->entity_test_mul_property_data_entity_test__field_test_data_i, 1); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['field_test_data']->id(), 1); + $this->assertEqual($row->_relationship_entities['field_test_data']->bundle(), 'entity_test_mul'); + + } + + // Check the backwards reference view. + $view = Views::getView('test_og_standard_reference_reverse_entity_test_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + $this->assertEqual($row->id, 1); + $this->assertEqual($row->_entity->id(), 1); + + // Test the backwards relationship. + $this->assertEqual($row->field_test_data_entity_test_mul_property_data_id, $this->entities[$index]->id()); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['reverse__entity_test__field_test_data']->id(), $this->entities[$index]->id()); + $this->assertEqual($row->_relationship_entities['reverse__entity_test__field_test_data']->bundle(), 'entity_test'); + } + } + + /** + * Tests views data generated for relationship. + * + * @see entity_reference_field_views_data() + */ + public function testDataTableRelationship() { + + // Create some test entities which link each other. + $referenced_entity = EntityTest::create(); + $referenced_entity->save(); + + $entity = EntityTestMul::create(); + $entity->field_data_test->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_data_test[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + $entity = EntityTestMul::create(); + $entity->field_data_test->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_data_test[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + Views::viewsData()->clear(); + + // Check the generated views data. + $views_data = Views::viewsData()->get('entity_test_mul__field_data_test'); + $this->assertEqual($views_data['field_data_test']['relationship']['id'], 'standard'); + $this->assertEqual($views_data['field_data_test']['relationship']['base'], 'entity_test'); + $this->assertEqual($views_data['field_data_test']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['field_data_test']['relationship']['relationship field'], 'field_data_test_target_id'); + $this->assertEqual($views_data['field_data_test']['relationship']['entity type'], 'entity_test'); + + // Check the backwards reference. + $views_data = Views::viewsData()->get('entity_test'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['id'], 'entity_reverse'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['base'], 'entity_test_mul_property_data'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['field table'], 'entity_test_mul__field_data_test'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['field field'], 'field_data_test_target_id'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['field_name'], 'field_data_test'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['entity_type'], 'entity_test_mul'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['join_extra'][0], ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE]); + + // Check an actual test view. + $view = Views::getView('test_og_standard_reference_entity_test_mul_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + // Check that the actual ID of the entity is the expected one. + $this->assertEqual($row->id, $this->entities[$index]->id()); + + // Also check that we have the correct result entity. + $this->assertEqual($row->_entity->id(), $this->entities[$index]->id()); + + // Test the forward relationship. + $this->assertEqual($row->entity_test_entity_test_mul__field_data_test_id, 1); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['field_data_test']->id(), 1); + $this->assertEqual($row->_relationship_entities['field_data_test']->bundle(), 'entity_test'); + + } + + // Check the backwards reference view. + $view = Views::getView('test_og_standard_reference_reverse_entity_test_mul_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + $this->assertEqual($row->id, 1); + $this->assertEqual($row->_entity->id(), 1); + + // Test the backwards relationship. + $this->assertEqual($row->field_data_test_entity_test_id, $this->entities[$index]->id()); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['reverse__entity_test_mul__field_data_test']->id(), $this->entities[$index]->id()); + $this->assertEqual($row->_relationship_entities['reverse__entity_test_mul__field_data_test']->bundle(), 'entity_test_mul'); + } + } + +} diff --git a/tests/src/Kernel/Field/AudienceFieldFormatterTest.php b/tests/src/Kernel/Field/AudienceFieldFormatterTest.php new file mode 100644 index 000000000..757d47225 --- /dev/null +++ b/tests/src/Kernel/Field/AudienceFieldFormatterTest.php @@ -0,0 +1,47 @@ +get('plugin.manager.field.formatter'); + + $expected = [ + 'entity_reference_entity_id', + 'entity_reference_entity_view', + 'entity_reference_label', + ]; + + foreach ([OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE, OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE] as $field_type) { + $actual = array_keys($formatter_manager->getOptions($field_type)); + sort($actual); + $this->assertEquals($expected, $actual); + } + } + +} diff --git a/tests/src/Kernel/OgDeleteOrphansTest.php b/tests/src/Kernel/OgDeleteOrphansTest.php new file mode 100644 index 000000000..32861031c --- /dev/null +++ b/tests/src/Kernel/OgDeleteOrphansTest.php @@ -0,0 +1,255 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('node', 'node_access'); + $this->installSchema('system', ['queue', 'sequences']); + + /** @var \Drupal\og\OgDeleteOrphansPluginManager ogDeleteOrphansPluginManager */ + $this->ogDeleteOrphansPluginManager = \Drupal::service('plugin.manager.og.delete_orphans'); + + // Create a group entity type. + $group_bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'type' => $group_bundle, + 'name' => $this->randomString(), + ])->save(); + Og::groupManager()->addGroup('node', $group_bundle); + + // Create a group content entity type. + $group_content_bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'type' => $group_content_bundle, + 'name' => $this->randomString(), + ])->save(); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', $group_content_bundle); + + // Create group admin user. + $group_admin = User::create(['name' => $this->randomString()]); + $group_admin->save(); + + // Create a group. + $this->group = Node::create([ + 'title' => $this->randomString(), + 'type' => $group_bundle, + 'uid' => $group_admin->id(), + ]); + $this->group->save(); + + // Create a group content item. + $group_content = Node::create([ + 'title' => $this->randomString(), + 'type' => $group_content_bundle, + OgGroupAudienceHelper::DEFAULT_FIELD => [['target_id' => $this->group->id()]], + ]); + $group_content->save(); + } + + /** + * Tests that orphaned group content is deleted when the group is deleted. + * + * @param string $plugin_id + * The machine name of the plugin under test. + * @param bool $run_cron + * Whether or not cron jobs should be run as part of the test. + * @param bool $asynchronous + * Whether or not the actual deletion of the orphans happens in an + * asynchronous operation (e.g. pressing the button that launches the batch + * process). + * @param string $queue_id + * The ID of the queue that is used by the plugin under test. + * + * @dataProvider ogDeleteOrphansPluginProvider + */ + public function testDeleteOrphans($plugin_id, $run_cron, $asynchronous, $queue_id) { + // Turn on deletion of orphans in the configuration and configure the chosen + // plugin. + $this->config('og.settings') + ->set('delete_orphans', TRUE) + ->set('delete_orphans_plugin_id', $plugin_id) + ->save(); + + // Check that the queue is initially empty. + $this->assertQueueCount($queue_id, 0); + + // Check that the group owner has initially been subscribed to the group. + $this->assertUserMembershipCount(1); + + // Delete the group. + $this->group->delete(); + + // Check that 2 orphans are queued for asynchronous processing: 1 group + // content item and 1 user membership. + if ($asynchronous) { + $this->assertQueueCount($queue_id, 2); + } + + // Run cron jobs if needed. + if ($run_cron) { + $this->container->get('cron')->run(); + } + + // Simulate the initiation of the queue process by an asynchronous operation + // (such as pressing the button that starts a batch operation). + if ($asynchronous) { + $this->process($queue_id, $plugin_id); + } + + // Verify the group content is deleted. + $this->assertFalse($this->group_content, 'The orphaned node is deleted.'); + + // Verify that the user membership is now deleted. + $this->assertUserMembershipCount(0); + + // Check that the queue is now empty. + $this->assertQueueCount($queue_id, 0); + } + + /** + * Tests that orphaned content is not deleted when the option is disabled. + * + * @param string $plugin_id + * The machine name of the plugin under test. + * @param bool $run_cron + * Whether or not cron jobs should be run as part of the test. Unused in + * this test. + * @param bool $asynchronous + * Whether or not the actual deletion of the orphans happens in an + * asynchronous operation (e.g. pressing the button that launches the batch + * process). Unused in this test. + * @param string $queue_id + * The ID of the queue that is used by the plugin under test. + * + * @dataProvider ogDeleteOrphansPluginProvider + */ + function testDisabled($plugin_id, $run_cron, $asynchronous, $queue_id) { + // Disable deletion of orphans in the configuration and configure the chosen + // plugin. + $this->config('og.settings') + ->set('delete_orphans', FALSE) + ->set('delete_orphans_plugin_id', $plugin_id) + ->save(); + + // Delete the group. + $this->group->delete(); + + // Check that no orphans are queued for deletion. + $this->assertQueueCount($queue_id, 0); + } + + /** + * Provides OgDeleteOrphans plugins for the tests. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - A string containing the plugin name being tested. + * - A boolean indicating whether or not cron jobs should be run. + * - A boolean indicating whether the deletion happens in an asynchronous + * process. + * - A string defining the queue that is used by the plugin. + */ + public function ogDeleteOrphansPluginProvider() { + return [ + ['batch', FALSE, TRUE, 'og_orphaned_group_content'], + ['cron', TRUE, FALSE, 'og_orphaned_group_content_cron'], + ['simple', FALSE, FALSE, 'og_orphaned_group_content'], + ]; + } + + /** + * Returns the number of items a given queue contains. + * + * @param string $queue_id + * The ID of the queue for which to count the items. + */ + protected function getQueueCount($queue_id) { + return $this->container->get('queue')->get($queue_id)->numberOfItems(); + } + + /** + * Checks that the given queue contains the expected number of items. + * + * @param string $queue_id + * The ID of the queue to check. + * @param int $count + * The expected number of items in the queue. + */ + protected function assertQueueCount($queue_id, $count) { + $this->assertEquals($count, $this->getQueueCount($queue_id)); + } + + /** + * Checks the number of user memberships. + * + * @param int $expected + * The expected number of user memberships. + */ + protected function assertUserMembershipCount($expected) { + $count = \Drupal::entityQuery('og_membership')->count()->execute(); + $this->assertEquals($expected, $count); + } + + /** + * Processes the given queue. + * + * @param string $queue_id + * The ID of the queue to process. + * @param string $plugin_id + * The ID of the plugin that is responsible for processing the queue. + */ + protected function process($queue_id, $plugin_id) { + /** @var \Drupal\og\OgDeleteOrphansInterface $plugin */ + $plugin = $this->ogDeleteOrphansPluginManager->createInstance($plugin_id, []); + while ($this->getQueueCount($queue_id) > 0) { + $plugin->process(); + } + } + +} diff --git a/tests/src/Kernel/PermissionEventTest.php b/tests/src/Kernel/PermissionEventTest.php new file mode 100644 index 000000000..d06bf5c8b --- /dev/null +++ b/tests/src/Kernel/PermissionEventTest.php @@ -0,0 +1,148 @@ +eventDispatcher = $this->container->get('event_dispatcher'); + + // Create a group entity type. + $this->groupBundleId = 'test_group'; + NodeType::create([ + 'type' => $this->groupBundleId, + 'name' => $this->randomString(), + ])->save(); + Og::groupManager()->addGroup('node', $this->groupBundleId); + + // Create a group content entity type. + $group_content_bundle_id = 'test_group_content'; + NodeType::create([ + 'type' => $group_content_bundle_id, + 'name' => $this->randomString(), + ])->save(); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', $group_content_bundle_id); + } + + /** + * Tests that the two OG modules can provide their own OG permissions. + * + * Some permissions (such as 'subscribe', 'manage members', etc.) are + * available for all group types. In addition to this there are also OG + * permissions for creating, editing and deleting the group content that + * associated with the group. + * + * In this test we will check that the correct permissions are generated for + * our test group (which includes permissions to create, edit and delete group + * content of type 'test_group_content'), as well as a control group which + * doesn't have any group content - in this case it should only return the + * default permissions that are available to all group types. + * + * @param bool $test_group_content_permissions + * TRUE to check the permissions expected for a group type that has group + * content of type 'test_group_content'. FALSE to only check for the + * expected default permissions that are valid for any group type. + * @param array $expected_permissions + * An array of permission names that are expected to be returned. + * + * @dataProvider permissionEventDataProvider + */ + public function testPermissionEventIntegration($test_group_content_permissions, $expected_permissions) { + $entity_type_id = $test_group_content_permissions ? 'node' : $this->randomMachineName(); + $bundle_id = $test_group_content_permissions ? $this->groupBundleId : $this->randomMachineName(); + + // Retrieve the permissions from the listeners. + /** @var PermissionEvent $permission_event */ + $event = new PermissionEvent($entity_type_id, $bundle_id); + $permission_event = $this->eventDispatcher->dispatch(PermissionEventInterface::EVENT_NAME, $event); + $actual_permissions = array_keys($permission_event->getPermissions()); + + // Sort the permission arrays so they can be compared. + sort($expected_permissions); + sort($actual_permissions); + + $this->assertEquals($expected_permissions, $actual_permissions); + } + + /** + * Provides expected results for the testPermissionEventIntegration test. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - A boolean indication whether or not to request permissions for a group + * that has group content of type 'test_group_content'. If FALSE only the + * default permissions that are valid for any group type are returned. + * - An array of permission names that are expected to be returned. + */ + public function permissionEventDataProvider() { + $default_permissions = [ + 'add user', + 'administer group', + 'approve and deny subscription', + 'manage members', + 'manage permissions', + 'manage roles', + 'subscribe without approval', + 'subscribe', + 'unsubscribe', + 'update group', + ]; + $group_content_permissions = [ + 'create test_group_content node', + 'delete any test_group_content node', + 'delete own test_group_content node', + 'update any test_group_content node', + 'update own test_group_content node', + ]; + return [ + [FALSE, $default_permissions], + [TRUE, array_merge($default_permissions, $group_content_permissions)], + ]; + } + +} diff --git a/tests/src/Unit/CheckFieldCardinalityTest.php b/tests/src/Unit/CheckFieldCardinalityTest.php index e17051a4d..ac72363e6 100644 --- a/tests/src/Unit/CheckFieldCardinalityTest.php +++ b/tests/src/Unit/CheckFieldCardinalityTest.php @@ -86,7 +86,7 @@ public function testFieldCardinality($field_count, $cardinality, $expected) { ->willReturn($field_storage_definition_prophecy->reveal()) ->shouldBeCalled(); $field_definition_prophecy->getType() - ->willReturn('og_membership_reference') + ->willReturn(OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE) ->shouldBeCalled(); $entity_prophecy = $this->prophesize(ContentEntityInterface::class); diff --git a/tests/src/Unit/DefaultRoleEventTest.php b/tests/src/Unit/DefaultRoleEventTest.php new file mode 100644 index 000000000..3ce72d441 --- /dev/null +++ b/tests/src/Unit/DefaultRoleEventTest.php @@ -0,0 +1,485 @@ +setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $event->getRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::getRoles + * @covers ::setRoles + * + * @dataProvider defaultRoleProvider + */ + public function testGetRoles($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + $actual_roles = $event->getRoles(); + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $actual_roles[$name]); + } + + $this->assertEquals(count($roles), count($actual_roles)); + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::addRole + * + * @dataProvider defaultRoleProvider + */ + public function testAddRole($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event->addRole($name, $role); + $this->assertRoleEquals($role, $event->getRole($name)); + + // Adding a role a second time should throw an exception. + try { + $event->addRole($name, $role); + $this->fail('It should not be possible to add a role with the same name a second time.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::addRoles + * + * @dataProvider defaultRoleProvider + */ + public function testAddRoles($roles) { + $event = new DefaultRoleEvent(); + $event->addRoles($roles); + + $actual_roles = $event->getRoles(); + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $actual_roles[$name]); + } + + $this->assertEquals(count($roles), count($actual_roles)); + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::setRole + * + * @dataProvider defaultRoleProvider + */ + public function testSetRole($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event->setRole($name, $role); + $this->assertRoleEquals($role, $event->getRole($name)); + + // Setting a role a second time should be possible. No exception should be + // thrown. + $event->setRole($name, $role); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::deleteRole + * + * @dataProvider defaultRoleProvider + */ + public function testDeleteRole($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertTrue($event->hasRole($name)); + $event->deleteRole($name); + $this->assertFalse($event->hasRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::hasRole + * + * @dataProvider defaultRoleProvider + */ + public function testHasRole($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event->addRole($name, $role); + $this->assertTrue($event->hasRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetGet + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetGet($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $event[$name]); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetSet + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetSet($roles) { + $event = new DefaultRoleEvent(); + + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event[$name] = $role; + $this->assertRoleEquals($role, $event->getRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetUnset + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetUnset($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertTrue($event->hasRole($name)); + unset($event[$name]); + $this->assertFalse($event->hasRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetExists + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetExists($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse(isset($event[$name])); + $event->addRole($name, $role); + $this->assertTrue(isset($event[$name])); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::getIterator + * + * @dataProvider defaultRoleProvider + */ + public function testIteratorAggregate($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($event as $name => $role) { + $this->assertRoleEquals($roles[$name], $role); + unset($roles[$name]); + } + + // Verify that all roles were iterated over. + $this->assertEmpty($roles); + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::addRole + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testAddInvalidRole($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + foreach ($invalid_roles as $name => $invalid_role) { + $event->addRole($name, $invalid_role); + } + $this->fail('An invalid role cannot be added.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::addRoles + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testAddInvalidRoles($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + $event->addRoles($invalid_roles); + $this->fail('An array of invalid roles cannot be added.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::setRole + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testSetInvalidRole($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + foreach ($invalid_roles as $name => $invalid_role) { + $event->setRole($name, $invalid_role); + } + $this->fail('An invalid role cannot be set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::setRoles + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testSetInvalidRoles($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + $event->setRoles($invalid_roles); + $this->fail('An array of invalid roles cannot be set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::offsetSet + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testInvalidOffsetSet($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + foreach ($invalid_roles as $name => $invalid_role) { + $event[$name] = $invalid_role; + } + $this->fail('An invalid role cannot be set through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * Provides test data to test default roles. + * + * @return array + * An array of test data arrays, each test data array containing an array of + * test default roles, keyed by default role name. + */ + public function defaultRoleProvider() { + return [ + // Test adding a single administrator role with only a label. + [ + [ + OgRoleInterface::ADMINISTRATOR => ['label' => $this->t('Administrator')], + ], + ], + // Test adding a single administrator role with a label and role type. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + ], + ], + // Test adding multiple roles. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + 'moderator' => [ + 'label' => $this->t('Moderator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_STANDARD, + ], + 'contributor' => [ + 'label' => $this->t('Contributor'), + ], + ], + ], + ]; + } + + /** + * Provides invalid test data to test default roles. + * + * @return array + * An array of test data arrays, each test data array containing an array of + * invalid test default roles, keyed by default role name. + */ + public function invalidDefaultRoleProvider() { + return [ + // A role with a missing name. + [ + [ + '' => ['label' => $this->t('Administrator')], + ], + ], + // A role without a label. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + ], + ], + // A role with an invalid role type. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => 'Some non-existing role type', + ], + ], + ], + // An array of multiple correct roles, with one invalid role type sneaked + // in. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + 'moderator' => [ + 'label' => $this->t('Moderator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_STANDARD, + ], + 'contributor' => [ + 'label' => $this->t('Contributor'), + 'role_type' => 'Some non-existing role type', + ], + ], + ], + ]; + } + + /** + * Mock translation method. + * + * @param string $string + * The string to translate. + * + * @return string + * The translated string. + */ + protected function t($string) { + // Actually translating the strings is not important for this test. + return $string; + } + + /** + * Asserts that the given role properties matches the expected result. + * + * @param array $expected + * An array of expected role properties. + * @param array $actual + * An array of actual role properties. + */ + protected function assertRoleEquals(array $expected, array $actual) { + // Provide default value for the role type. + if (empty($expected['role_type'])) { + $expected['role_type'] = OgRoleInterface::ROLE_TYPE_STANDARD; + } + $this->assertEquals($expected, $actual); + } + +} diff --git a/tests/src/Unit/GroupManagerTest.php b/tests/src/Unit/GroupManagerTest.php index fc412b319..30a6ae2d5 100644 --- a/tests/src/Unit/GroupManagerTest.php +++ b/tests/src/Unit/GroupManagerTest.php @@ -7,8 +7,21 @@ namespace Drupal\Tests\og\Unit; -use Drupal\og\GroupManager; +use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\State\StateInterface; +use Drupal\og\Event\DefaultRoleEvent; +use Drupal\og\Event\DefaultRoleEventInterface; use Drupal\Tests\UnitTestCase; +use Drupal\og\Entity\OgRole; +use Drupal\og\Event\PermissionEventInterface; +use Drupal\og\GroupManager; +use Drupal\og\OgRoleInterface; +use Prophecy\Argument; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @group og @@ -26,34 +39,79 @@ class GroupManagerTest extends UnitTestCase { */ protected $configFactoryProphecy; + /** + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $entityTypeManagerProphecy; + + /** + * @var \Drupal\Core\Entity\EntityStorageInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $entityStorageProphecy; + + /** + * @var \Drupal\og\Entity\OgRole|\Prophecy\Prophecy\ObjectProphecy + */ + protected $ogRoleProphecy; + + /** + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $entityTypeBundleInfoProphecy; + + /** + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $eventDispatcherProphecy; + + /** + * @var \Drupal\og\Event\PermissionEventInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $permissionEventProphecy; + + /** + * @var \Drupal\Core\State\StateInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $stateProphecy; + + /** + * @var \Drupal\og\Event\DefaultRoleEventInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $defaultRoleEventProphecy; + /** * {@inheritdoc} */ public function setUp() { - $this->configProphecy = $this->prophesize('Drupal\Core\Config\Config'); - $this->configFactoryProphecy = $this->prophesize('Drupal\Core\Config\ConfigFactoryInterface'); + $this->configProphecy = $this->prophesize(Config::class); + $this->configFactoryProphecy = $this->prophesize(ConfigFactoryInterface::class); + $this->entityTypeManagerProphecy = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityStorageProphecy = $this->prophesize(EntityStorageInterface::class); + $this->ogRoleProphecy = $this->prophesize(OgRole::class); + $this->entityTypeBundleInfoProphecy = $this->prophesize(EntityTypeBundleInfoInterface::class); + $this->eventDispatcherProphecy = $this->prophesize(EventDispatcherInterface::class); + $this->permissionEventProphecy = $this->prophesize(PermissionEventInterface::class); + $this->stateProphecy = $this->prophesize(StateInterface::class); + $this->defaultRoleEventProphecy = $this->prophesize(DefaultRoleEvent::class); } /** * @covers ::__construct */ public function testInstance() { - $this->configProphecy->get('groups') - ->shouldBeCalled(); - - // Just creating an instance should not get the 'groups' config key. - $this->createGroupManager(); + // Just creating an instance should be lightweight, no methods should be + // called. + $group_manager = $this->createGroupManager(); + $this->assertInstanceOf(GroupManager::class, $group_manager); } /** * @covers ::getAllGroupBundles */ public function testGetAllGroupBundles() { + // It is expected that the group map will be retrieved from config. $groups = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups); $manager = $this->createGroupManager(); @@ -66,11 +124,9 @@ public function testGetAllGroupBundles() { * @dataProvider providerTestIsGroup */ public function testIsGroup($entity_type_id, $bundle_id, $expected) { + // It is expected that the group map will be retrieved from config. $groups = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups); $manager = $this->createGroupManager(); @@ -96,11 +152,9 @@ public function providerTestIsGroup() { * @covers ::getGroupsForEntityType */ public function testGetGroupsForEntityType() { + // It is expected that the group map will be retrieved from config. $groups = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups); $manager = $this->createGroupManager(); @@ -112,24 +166,12 @@ public function testGetGroupsForEntityType() { * @covers ::addGroup */ public function testAddGroupExisting() { - $this->configFactoryProphecy->getEditable('og.settings') - ->willReturn($this->configProphecy->reveal()) - ->shouldBeCalled(); - + // It is expected that the group map will be retrieved from config. $groups_before = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups_before) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups_before); $groups_after = ['test_entity' => ['a', 'b', 'c']]; - $this->configProphecy->set('groups', $groups_after) - ->shouldBeCalled(); - - $this->configProphecy->save() - ->shouldBeCalled(); - $this->configProphecy->get('groups') ->willReturn($groups_after) ->shouldBeCalled(); @@ -137,9 +179,13 @@ public function testAddGroupExisting() { $manager = $this->createGroupManager(); // Add to existing. - $manager->addGroup('test_entity', 'c'); - $this->assertSame(['a', 'b', 'c'], $manager->getGroupsForEntityType('test_entity')); - $this->assertTrue($manager->isGroup('test_entity', 'c')); + try { + $manager->addGroup('test_entity', 'c'); + $this->fail('An entity type that was already declared as a group, was redeclared as a group'); + } catch (\InvalidArgumentException $e) { + // Expected result. An exception should be thrown when an entity type is + // redeclared as a group. + } } /** @@ -150,26 +196,28 @@ public function testAddGroupNew() { ->willReturn($this->configProphecy->reveal()) ->shouldBeCalled(); + // It is expected that the group map will be retrieved from config. $groups_before = []; - - $this->configProphecy->get('groups') - ->willReturn($groups_before) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups_before); $groups_after = ['test_entity_new' => ['a']]; + $config_prophecy = $this->configProphecy; $this->configProphecy->set('groups', $groups_after) + ->will(function () use ($groups_after, $config_prophecy) { + $config_prophecy->get('groups') + ->willReturn($groups_after) + ->shouldBeCalled(); + }) ->shouldBeCalled(); $this->configProphecy->save() ->shouldBeCalled(); - $this->configProphecy->get('groups') - ->willReturn($groups_after) - ->shouldBeCalled(); - $manager = $this->createGroupManager(); + $this->expectDefaultRoleCreation('test_entity_new', 'a'); + // Add a new entity type. $manager->addGroup('test_entity_new', 'a'); $this->assertSame(['a'], $manager->getGroupsForEntityType('test_entity_new')); @@ -184,11 +232,9 @@ public function testRemoveGroup() { ->willReturn($this->configProphecy->reveal()) ->shouldBeCalled(); + // It is expected that the group map will be retrieved from config. $groups_before = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups_before) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups_before); $groups_after = ['test_entity' => ['a']]; @@ -202,6 +248,8 @@ public function testRemoveGroup() { ->willReturn($groups_after) ->shouldBeCalled(); + $this->expectRoleRemoval('test_entity', 'b'); + $manager = $this->createGroupManager(); // Add to existing. @@ -217,11 +265,129 @@ public function testRemoveGroup() { * @return \Drupal\og\GroupManager */ protected function createGroupManager() { + // It is expected that the role storage will be initialized. + $this->entityTypeManagerProphecy->getStorage('og_role') + ->willReturn($this->entityStorageProphecy->reveal()) + ->shouldBeCalled(); + + return new GroupManager( + $this->configFactoryProphecy->reveal(), + $this->entityTypeManagerProphecy->reveal(), + $this->entityTypeBundleInfoProphecy->reveal(), + $this->eventDispatcherProphecy->reveal(), + $this->stateProphecy->reveal() + ); + } + + /** + * Sets up an expectation that the group map will be retrieved from config. + * + * @param array $groups + * The expected group map that will be returned by the mocked config. + */ + protected function expectGroupMapRetrieval($groups = []) { $this->configFactoryProphecy->get('og.settings') ->willReturn($this->configProphecy->reveal()) ->shouldBeCalled(); - return new GroupManager($this->configFactoryProphecy->reveal()); + $this->configProphecy->get('groups') + ->willReturn($groups) + ->shouldBeCalled(); + } + + /** + * Mocked method calls when system under test should create default roles. + * + * @param string $entity_type + * The entity type for which default roles should be created. + * @param string $bundle + * The bundle for which default roles should be created. + */ + protected function expectDefaultRoleCreation($entity_type, $bundle) { + // In order to populate the default roles for a new group type, it is + // expected that the list of default roles to populate will be retrieved + // from the event listener. + $this->eventDispatcherProphecy->dispatch(DefaultRoleEventInterface::EVENT_NAME, Argument::type(DefaultRoleEvent::class)) + ->willReturn($this->defaultRoleEventProphecy->reveal()) + ->shouldBeCalled(); + $this->defaultRoleEventProphecy->getRoles() + ->willReturn([]) + ->shouldBeCalled(); + + foreach ([OgRoleInterface::ANONYMOUS, OgRoleInterface::AUTHENTICATED] as $role_name) { + $this->addNewDefaultRole($entity_type, $bundle, $role_name); + } + } + + /** + * Expected method calls when creating a new default role. + * + * @param string $entity_type + * The entity type for which the default role should be created. + * @param string $bundle + * The bundle for which the default role should be created. + * @param string $role_name + * The name of the role being created. + */ + protected function addNewDefaultRole($entity_type, $bundle, $role_name) { + // It is expected that the OG permissions that need to be populated on the + // new role will be requested from the PermissionEvent listener. + $this->eventDispatcherProphecy->dispatch(PermissionEventInterface::EVENT_NAME, Argument::type('\Drupal\og\Event\PermissionEvent')) + ->willReturn($this->permissionEventProphecy->reveal()) + ->shouldBeCalled(); + $this->permissionEventProphecy->filterByDefaultRole($role_name) + ->willReturn([]) + ->shouldBeCalled(); + + // It is expected that the role will be created with default properties. + $properties = [ + 'group_type' => $entity_type, + 'group_bundle' => $bundle, + 'role_type' => OgRole::getRoleTypeByName($role_name), + 'id' => $role_name, + 'permissions' => [], + ]; + $this->entityStorageProphecy->create($properties + OgRole::getDefaultRoles()[$role_name]) + ->willReturn($this->ogRoleProphecy->reveal()) + ->shouldBeCalled(); + + // The role is expected to be saved. + $this->ogRoleProphecy->save() + ->willReturn(1) + ->shouldBeCalled(); + } + + /** + * Expected method calls when deleting roles after a group is deleted. + * + * @param string $entity_type_id + * The entity type for which the roles should be deleted. + * @param string $bundle_id + * The bundle for which the roles should be deleted. + */ + protected function expectRoleRemoval($entity_type_id, $bundle_id) { + // It is expected that a call is done to retrieve all roles associated with + // the group. This will return the 3 default role entities. + $this->entityTypeManagerProphecy->getStorage('og_role') + ->willReturn($this->entityStorageProphecy->reveal()) + ->shouldBeCalled(); + + $properties = [ + 'group_type' => $entity_type_id, + 'group_bundle' => $bundle_id, + ]; + $this->entityStorageProphecy->loadByProperties($properties) + ->willReturn([ + $this->ogRoleProphecy->reveal(), + $this->ogRoleProphecy->reveal(), + $this->ogRoleProphecy->reveal(), + ]) + ->shouldBeCalled(); + + // It is expected that all roles will be deleted, so three delete() calls + // will be made. + $this->ogRoleProphecy->delete() + ->shouldBeCalledTimes(3); } } diff --git a/tests/src/Unit/OgAccessEntityTest.php b/tests/src/Unit/OgAccessEntityTest.php index 194730e48..9e85d3db4 100644 --- a/tests/src/Unit/OgAccessEntityTest.php +++ b/tests/src/Unit/OgAccessEntityTest.php @@ -20,11 +20,15 @@ class OgAccessEntityTest extends OgAccessEntityTestBase { * @coversDefaultmethod ::userAccessEntity * @dataProvider operationProvider */ - public function testDefaultForbidden($operation) { + public function testAccessByOperation($operation) { $group_entity = $this->groupEntity(); $group_entity->isNew()->willReturn(FALSE); - $user_access = OgAccess::userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); - $this->assertTrue($user_access->isForbidden()); + $user_access = $this->ogAccess->userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); + + // We populate the allowed permissions cache in + // OgAccessEntityTestBase::setup(). + $condition = $operation == 'update group' ? $user_access->isAllowed() : $user_access->isForbidden(); + $this->assertTrue($condition); } /** @@ -34,7 +38,7 @@ public function testDefaultForbidden($operation) { public function testEntityNew($operation) { $group_entity = $this->groupEntity(); $group_entity->isNew()->willReturn(TRUE); - $user_access = OgAccess::userAccessEntity($operation, $group_entity->reveal(), $this->user->reveal()); + $user_access = $this->ogAccess->userAccessEntity($operation, $group_entity->reveal(), $this->user->reveal()); $this->assertTrue($user_access->isNeutral()); } @@ -44,7 +48,7 @@ public function testEntityNew($operation) { */ public function testGetEntityGroups($operation) { $this->user->hasPermission(OgAccess::ADMINISTER_GROUP_PERMISSION)->willReturn(TRUE); - $user_entity_access = OgAccess::userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); + $user_entity_access = $this->ogAccess->userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); $this->assertTrue($user_entity_access->isAllowed()); } diff --git a/tests/src/Unit/OgAccessEntityTestBase.php b/tests/src/Unit/OgAccessEntityTestBase.php index 0fefac478..479598e33 100644 --- a/tests/src/Unit/OgAccessEntityTestBase.php +++ b/tests/src/Unit/OgAccessEntityTestBase.php @@ -8,10 +8,16 @@ namespace Drupal\Tests\og\Unit; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\og\OgAccessInterface; +use Drupal\og\OgGroupAudienceHelper; +use Prophecy\Argument; class OgAccessEntityTestBase extends OgAccessTestBase { @@ -21,7 +27,7 @@ public function setup() { parent::setUp(); $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getType()->willReturn('og_membership_reference'); + $field_definition->getType()->willReturn(OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE); $field_definition->getFieldStorageDefinition() ->willReturn($this->prophesize(FieldStorageDefinitionInterface::class)->reveal()); $field_definition->getSetting("handler_settings")->willReturn([]); @@ -29,10 +35,13 @@ public function setup() { $entity_type_id = $this->randomMachineName(); $bundle = $this->randomMachineName(); - $entity_id = mt_rand(20, 30); + + // Just a random entity ID. + $entity_id = 20; $entity_type = $this->prophesize(EntityTypeInterface::class); $entity_type->getListCacheTags()->willReturn([]); + $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); $entity_type->id()->willReturn($entity_type_id); $this->entity = $this->prophesize(ContentEntityInterface::class); @@ -44,15 +53,24 @@ public function setup() { $this->groupManager->isGroup($entity_type_id, $bundle)->willReturn(FALSE); - $entity_manager = $this->prophesize(EntityManagerInterface::class); - $entity_manager->getFieldDefinitions($entity_type_id, $bundle)->willReturn([$field_definition->reveal()]); - \Drupal::getContainer()->set('entity.manager', $entity_manager->reveal()); + $entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class); + $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle)->willReturn([$field_definition->reveal()]); + + $group_type_id = $this->group->getEntityTypeId(); + + $storage = $this->prophesize(EntityStorageInterface::class); - // Mock the results of Og::getEntityGroups(). - $r = new \ReflectionClass('Drupal\og\Og'); - $reflection_property = $r->getProperty('entityGroupCache'); - $reflection_property->setAccessible(TRUE); - $reflection_property->setValue(["$entity_type_id:$entity_id:1:" => [[$this->groupEntity()->reveal()]]]); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getDefinition($entity_type_id)->willReturn($entity_type->reveal()); + $entity_type_manager->getStorage($group_type_id)->willReturn($storage->reveal()); + + $container = \Drupal::getContainer(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + $container->set('entity_field.manager', $entity_field_manager->reveal()); + + // Mock the results of Og::getGroups(). + $storage->loadMultiple(Argument::type('array'))->willReturn([$this->group]); } + } diff --git a/tests/src/Unit/OgAccessTest.php b/tests/src/Unit/OgAccessTest.php index 615777f51..bfa569035 100644 --- a/tests/src/Unit/OgAccessTest.php +++ b/tests/src/Unit/OgAccessTest.php @@ -21,7 +21,7 @@ class OgAccessTest extends OgAccessTestBase { */ public function testUserAccessNotAGroup($operation) { $this->groupManager->isGroup($this->entityTypeId, $this->bundle)->willReturn(FALSE); - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation); + $user_access = $this->ogAccess->userAccess($this->group, $operation); $this->assertTrue($user_access->isNeutral()); } @@ -29,9 +29,14 @@ public function testUserAccessNotAGroup($operation) { * @coversDefaultmethod ::userAccess * @dataProvider operationProvider */ - public function testUserAccessForbiddenByDefault($operation) { - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation, $this->user->reveal()); - $this->assertTrue($user_access->isForbidden()); + public function testAccessByOperation($operation) { + $user_access = $this->ogAccess->userAccess($this->group, $operation, $this->user->reveal()); + + // We populate the allowed permissions cache in + // OgAccessTestBase::setup(). + $condition = $operation == 'update group' ? $user_access->isAllowed() : $user_access->isForbidden(); + + $this->assertTrue($condition); } /** @@ -40,7 +45,7 @@ public function testUserAccessForbiddenByDefault($operation) { */ public function testUserAccessUser1($operation) { $this->user->id()->willReturn(1); - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation, $this->user->reveal()); + $user_access = $this->ogAccess->userAccess($this->group, $operation, $this->user->reveal()); $this->assertTrue($user_access->isAllowed()); } @@ -50,7 +55,7 @@ public function testUserAccessUser1($operation) { */ public function testUserAccessAdminPermission($operation) { $this->user->hasPermission(OgAccess::ADMINISTER_GROUP_PERMISSION)->willReturn(TRUE); - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation, $this->user->reveal()); + $user_access = $this->ogAccess->userAccess($this->group, $operation, $this->user->reveal()); $this->assertTrue($user_access->isAllowed()); } @@ -60,30 +65,8 @@ public function testUserAccessAdminPermission($operation) { */ public function testUserAccessOwner($operation) { $this->config->get('group_manager_full_access')->willReturn(TRUE); - $user_access = OgAccess::userAccess($this->groupEntity(TRUE)->reveal(), $operation, $this->user->reveal()); + $user_access = $this->ogAccess->userAccess($this->groupEntity(TRUE)->reveal(), $operation, $this->user->reveal()); $this->assertTrue($user_access->isAllowed()); } - /** - * @coversDefaultmethod ::userAccess - * @dataProvider operationProvider - */ - public function testUserAccessOgUserAccessAlter($operation) { - $permissions[OgAccess::ADMINISTER_GROUP_PERMISSION] = TRUE; - \Drupal::getContainer()->set('module_handler', new OgAccessTestAlter($permissions)); - $group_entity = $this->groupEntity(); - $group_entity->id()->willReturn(mt_rand(5, 10)); - $user_access = OgAccess::userAccess($group_entity->reveal(), $operation, $this->user->reveal()); - $this->assertTrue($user_access->isAllowed()); - } - -} - -class OgAccessTestAlter { - public function __construct($data) { - $this->data = $data; - } - public function alter($op, &$data) { - $data = $this->data; - } } diff --git a/tests/src/Unit/OgAccessTestBase.php b/tests/src/Unit/OgAccessTestBase.php index 6291b43e5..e8ba2ecde 100644 --- a/tests/src/Unit/OgAccessTestBase.php +++ b/tests/src/Unit/OgAccessTestBase.php @@ -7,17 +7,19 @@ namespace Drupal\Tests\og\Unit; -use Drupal\user\EntityOwnerInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Cache\Context\CacheContextsManager; use Drupal\Core\Config\Config; -use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\og\OgAccess; -use Drupal\Tests\UnitTestCase; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Tests\UnitTestCase; use Drupal\og\GroupManager; +use Drupal\og\OgAccess; +use Drupal\og\OgMembershipInterface; +use Drupal\user\EntityOwnerInterface; use Prophecy\Argument; class OgAccessTestBase extends UnitTestCase { @@ -39,11 +41,23 @@ class OgAccessTestBase extends UnitTestCase { */ protected $bundle; + protected $group; + /** * @var \Drupal\og\GroupManager */ protected $groupManager; + /** + * The OgAccess class, this is the system under test. + * + * @var \Drupal\og\OgAccessInterface + */ + protected $ogAccess; + + /** + * {@inheritdoc} + */ public function setUp() { $this->entityTypeId = $this->randomMachineName(); $this->bundle = $this->randomMachineName(); @@ -54,12 +68,25 @@ public function setUp() { $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); + // It is expected that any access check will retrieve the settings, because + // it contains an option to give full access to to the group manager. $this->config = $this->addCache($this->prophesize(Config::class)); $this->config->get('group_manager_full_access')->willReturn(FALSE); - $config_factory = $this->prophesize(ConfigFactory::class); + // Whether or not the user has access to a certain operation depends in part + // on the 'group_manager_full_access' setting which is stored in config. + // Since the access is cached, this means that from the point of view from + // the caching system this access varies by the 'og.settings' config object + // that contains this setting. It is hence expected that the cacheability + // metadata is retrieved from the config object so it can be attached to the + // access result object. + $config_factory = $this->prophesize(ConfigFactoryInterface::class); $config_factory->get('og.settings')->willReturn($this->config); + $this->config->getCacheContexts()->willReturn([]); + $this->config->getCacheTags()->willReturn([]); + $this->config->getCacheMaxAge()->willReturn(0); + $this->user = $this->prophesize(AccountInterface::class); $this->user->isAuthenticated()->willReturn(TRUE); $this->user->id()->willReturn(2); @@ -73,6 +100,65 @@ public function setUp() { // This is for caching purposes only. $container->set('current_user', $this->user->reveal()); \Drupal::setContainer($container); + + $this->group = $this->groupEntity()->reveal(); + $group_type_id = $this->group->getEntityTypeId(); + + $entity_id = 20; + + $account_proxy = $this->prophesize(AccountProxyInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + // Instantiate the system under test. + $this->ogAccess = new OgAccess($config_factory->reveal(), $account_proxy->reveal(), $module_handler->reveal()); + + // Set the Og::cache property values, to skip calculations. + $values = []; + + $r = new \ReflectionClass('Drupal\og\Og'); + $reflection_property = $r->getProperty('cache'); + $reflection_property->setAccessible(TRUE); + + // Mock the results of Og::getGroupIds(). + $identifier = [ + 'Drupal\og\Og::getGroupIds', + $entity_id, + NULL, + NULL, + ]; + + $identifier = implode(':', $identifier); + + $group_ids = [$group_type_id => [$this->group->id()]]; + $values[$identifier] = $group_ids; + + // Mock the results of Og::getUserMemberships(). + $identifier = [ + 'Drupal\og\Og::getUserMemberships', + 2, + OgMembershipInterface::STATE_ACTIVE, + // The field name. + NULL, + ]; + $identifier = implode(':', $identifier); + + // The cache is supposed to be holding the OG memberships, however it is not + // used in the tests, so we just set an empty array. + $values[$identifier] = []; + + $reflection_property->setValue($values); + + // Set the allowed permissions cache. + $r = new \ReflectionClass('Drupal\og\OgAccess'); + $reflection_property = $r->getProperty('permissionsCache'); + $reflection_property->setAccessible(TRUE); + + $values = []; + foreach (['pre_alter', 'post_alter'] as $key) { + $values[$group_type_id][$this->group->id()][2][$key] = ['permissions' => ['update group'], 'is_admin' => FALSE]; + } + + $reflection_property->setValue($this->ogAccess, $values); } /** @@ -104,8 +190,10 @@ protected function addCache($prophecy) { public function operationProvider() { return [ - ['view'], - ['update'], + // In the unit tests we don't really care about the permission name - it + // can be an arbitrary string; except for OgAccessTest::testUserAccessAdminPermission + // test which checks for "administer group" permission. + ['update group'], ['administer group'], ]; } diff --git a/tests/src/Unit/PermissionEventTest.php b/tests/src/Unit/PermissionEventTest.php new file mode 100644 index 000000000..0c6f2092c --- /dev/null +++ b/tests/src/Unit/PermissionEventTest.php @@ -0,0 +1,464 @@ + $permission) { + try { + $event->getPermission($name); + $this->fail('Calling ::getPermission() on a non-existing permission throws an exception.'); + } catch (\InvalidArgumentException $e) { + // Expected result. + } + } + + // Test that it can retrieve the permissions correctly after they are set. + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + $this->assertEquals($permission, $event->getPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getPermissions + * @covers ::setPermissions + * + * @dataProvider permissionsProvider + */ + public function testGetPermissions($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + $this->assertEquals($permissions, $event->getPermissions()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::setPermission + * + * @dataProvider permissionsProvider + */ + public function testSetPermission($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + // Test that an exception is thrown when setting a nameless permission. + try { + $event->setPermission('', ['title' => 'A permission without a name']); + $this->fail('An exception is thrown when a nameless permission is set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + // Test that an exception is thrown when setting permission without a title. + try { + $event->setPermission('an-invalid-permission', []); + $this->fail('An exception is thrown when a permission without a title is set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + foreach ($permissions as $name => $permission) { + $event->setPermission($name, $permission); + } + + $this->assertEquals($permissions, $event->getPermissions()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::deletePermission + * + * @dataProvider permissionsProvider + */ + public function testDeletePermission($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + // Before we delete the permission, it should still be there. + $this->assertTrue($event->hasPermission($name)); + + // After we delete the permission, it should be gone. + $event->deletePermission($name); + $this->assertFalse($event->hasPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::hasPermission + * + * @dataProvider permissionsProvider + */ + public function testHasPermission($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + foreach ($permissions as $name => $permission) { + $this->assertFalse($event->hasPermission($name)); + $event->setPermission($name, $permission); + $this->assertTrue($event->hasPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getEntityTypeId + * + * @dataProvider permissionsProvider + */ + public function testGetEntityTypeId($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $this->assertEquals($entity_type_id, $event->getEntityTypeId()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getBundleId + * + * @dataProvider permissionsProvider + */ + public function testGetBundleId($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $this->assertEquals($bundle_id, $event->getBundleId()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::filterByDefaultRole + * + * @dataProvider permissionsProvider + */ + public function testFilterByDefaultRole($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + $default_roles = [ + OgRoleInterface::ANONYMOUS, + OgRoleInterface::AUTHENTICATED, + OgRoleInterface::ADMINISTRATOR, + ]; + foreach ($default_roles as $default_role) { + $expected = array_filter($permissions, function ($permission) use ($default_role) { + return !empty($permission['default roles']) && in_array($default_role, $permission['default roles']); + }); + $this->assertEquals($expected, $event->filterByDefaultRole($default_role)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetGet + * + * @dataProvider permissionsProvider + */ + public function testOffsetGet($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + $this->assertEquals($permission, $event[$name]); + } + + // Test that an exception is thrown when requesting a non-existing + // permission. + try { + $event['some-non-existing-permission']; + $this->fail('An exception is thrown when a non-existing permission is requested through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetSet + * + * @dataProvider permissionsProvider + */ + public function testOffsetSet($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + // Test that an exception is thrown when setting a nameless permission. + try { + $event[] = ['title' => 'A permission without a name']; + $this->fail('An exception is thrown when a nameless permission is set through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + // Test that an exception is thrown when setting permission without a title. + try { + $event['an-invalid-permission'] = []; + $this->fail('An exception is thrown when a permission without a title is set through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + foreach ($permissions as $name => $permission) { + $this->assertFalse($event->hasPermission($name)); + $event[$name] = $permission; + $this->assertEquals($permission, $event->getPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetUnset + * + * @dataProvider permissionsProvider + */ + public function testOffsetUnset($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + $this->assertTrue($event->hasPermission($name)); + unset($event[$name]); + $this->assertFalse($event->hasPermission($name)); + } + + // @todo + // Test that an exception is thrown when unsetting a non-existing + // permission. + try { + $event['some-non-existing-permission']; + $this->fail('An exception is thrown when a non-existing permission is requested through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetExists + * + * @dataProvider permissionsProvider + */ + public function testOffsetExists($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + foreach ($permissions as $name => $permission) { + $this->assertFalse(isset($event[$name])); + $event->setPermission($name, $permission); + $this->assertTrue(isset($event[$name])); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getIterator + * + * @dataProvider permissionsProvider + */ + public function testIteratorAggregate($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($event as $name => $permission) { + $expected_permission = reset($permissions); + $expected_name = key($permissions); + $this->assertEquals($expected_name, $name); + $this->assertEquals($expected_permission, $permission); + array_shift($permissions); + } + + // Check that the iterator has looped over all permissions correctly. + $this->assertEmpty($permissions); + } + + /** + * Data provider to test permissions. + * + * @return array + * An array of test data arrays, each test data array containing: + * - An array of test permissions, keyed by permission ID. + * - The entity type ID of the group type to which these permissions apply. + * - The bundle ID of the group type to which these permissions apply. + */ + public function permissionsProvider() { + $permissions = [ + // A simple permission with only the required option. + [ + [ + 'appreciate nature' => [ + 'title' => $this->t('Allows the member to go outdoors and appreciate the landscape.'), + ], + ], + ], + // A single permission with restricted access and a default role. + [ + [ + 'administer group' => [ + 'title' => $this->t('Administer group'), + 'description' => $this->t('Manage group members and content in the group.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + 'restrict access' => TRUE, + ], + ], + ], + // A permission restricted to a specific role, and having a default role. + [ + [ + 'unsubscribe' => [ + 'title' => $this->t('Unsubscribe from group'), + 'description' => $this->t('Allow members to unsubscribe themselves from a group, removing their membership.'), + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'default roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + // Simulate a subscriber providing multiple permissions. + [ + [ + 'subscribe' => [ + 'title' => $this->t('Subscribe to group'), + 'description' => $this->t('Allow non-members to request membership to a group (approval required).'), + 'roles' => [OgRoleInterface::ANONYMOUS], + 'default roles' => [OgRoleInterface::ANONYMOUS], + ], + 'subscribe without approval' => [ + 'title' => $this->t('Subscribe to group (no approval required)'), + 'description' => $this->t('Allow non-members to join a group without an approval from group administrators.'), + 'roles' => [OgRoleInterface::ANONYMOUS], + ], + 'unsubscribe' => [ + 'title' => $this->t('Unsubscribe from group'), + 'description' => $this->t('Allow members to unsubscribe themselves from a group, removing their membership.'), + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'default roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + ]; + + // Supply a random entity type ID and bundle ID for each data set. + foreach ($permissions as &$item) { + $item[] = $this->randomMachineName(); + $item[] = $this->randomMachineName(); + } + + return $permissions; + } + + /** + * Mock translation method. + * + * @param string $string + * The string to translate. + * + * @return string + * The translated string. + */ + protected function t($string) { + // Actually translating the strings is not important for this test. + return $string; + } + +} +