monster_menus-9.0.x-dev/tests/src/Functional/Permissions/PermissionsTest.php
tests/src/Functional/Permissions/PermissionsTest.php
<?php
namespace Drupal\Tests\monster_menus\Functional\Permissions;
use Drupal\Core\Database\Database;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\monster_menus\Constants;
use Drupal\monster_menus\MMCreatePath\MMCreatePath;
use Drupal\monster_menus\MMCreatePath\MMCreatePathCat;
use Drupal\monster_menus\MMCreatePath\MMCreatePathGroup;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
/**
* @group MonsterMenus
*/
class PermissionsTest extends BrowserTestBase {
protected static $modules = ['monster_menus'];
protected $defaultTheme = 'stark';
/**
* If TRUE, generate and save the baseline file.
*/
final public const generateBaseline = FALSE;
/**
* If TRUE, test node permissions.
*/
final public const testNodes = TRUE;
final public const baselineFilename = 'PermissionsTestBaseline.yml.gz';
private $uids = [];
/** @var MMCreatePath $mmCreatePath */
private $mmCreatePath;
private $testNodes;
public $baseline;
/**
* @inheritDoc
*/
protected function setUp(): void {
parent::setUp();
// Prevent mm_content_delete() from printing deletion stats.
$_SERVER['SERVER_SOFTWARE'] = 'test';
$setup_user = function($access_modes) {
$label = join('+', $access_modes);
$role = Role::create(['id' => $this->randomMachineName(8), 'label' => "$label Role"]);
foreach ($access_modes as $access_mode) {
$role->grantPermission($access_mode);
}
$role->save();
$user = User::create(['name' => "Can $label", 'status' => 1]);
$user->addRole($role->id());
$user->save();
$this->uids[$label] = $user->id();
};
$this->testNodes = self::testNodes && mm_module_exists('node');
$this->mmCreatePath = $this->container->get('monster_menus.mm_create_path');
$this->uids = ['anonymous' => 0, 'admin' => 1];
$access_modes = ['administer all menus', 'administer all users', 'administer all groups', 'view all menus', 'bypass node access'];
foreach ($access_modes as $access_mode) {
if ($access_mode != 'bypass node access' || $this->testNodes) {
$setup_user([$access_mode]);
}
}
if ($this->testNodes) {
$setup_user(['administer all menus', 'bypass node access']);
}
$user = User::create(['name' => 'No roles', 'roles' => [], 'status' => 1]);
$user->save();
$this->uids['no roles'] = $user->id();
if (!self::generateBaseline) {
try {
$this->baseline = Yaml::decode(join("\n", gzfile(__DIR__ . '/' . self::baselineFilename)));
}
catch (\Exception $e) {
print('Could not open baseline file ' . __DIR__ . '/' . self::baselineFilename . ': ' . $e->getMessage());
throw $e;
}
}
}
/**
* @throws \Exception
*/
public function testPermissions() {
// The following will turn off devel query logging, to make this test as
// fast as possible.
Database::getLog('devel');
$database = Database::getConnection();
$test_node = function ($usr, $print_path, NodeInterface $node, &$stats, $pass, $curr, $secondary) {
$label = $node->label();
$new = mm_content_user_can_node($node, '', $usr);
foreach ([Constants::MM_PERMS_READ, Constants::MM_PERMS_WRITE] as $mode) {
$stats['count']++;
if (self::generateBaseline) {
$stats['baseline'][$print_path]['node'][$label][$mode][$pass] = !empty($new[$mode]);
}
else if (!isset($this->baseline[$stats['label']][$print_path]['node'][$label][$mode][$pass])) {
$stats['fail'][] = $this->failed("Undefined baseline entry ['" . $stats['label'] . "']['$print_path']['node']['$label']['$mode'][$pass]. Re-run this test using PermissionsTest::wantBaseline=TRUE and use the output to rewrite " . self::baselineFilename . '.');
}
else {
$old = $this->baseline[$stats['label']][$print_path]['node'][$label][$mode][$pass];
if ($new[$mode] != $old) {
if (!$curr) {
$stats['fail'][] = $this->failed("$print_path: node '$label' pass $pass: $mode", $old);
}
else if ($pass) {
$stats['fail'][] = $this->failed("($curr->mmtid) and ($secondary->mmtid) $print_path: node '$label': $mode", $old);
}
else {
$stats['fail'][] = $this->failed("($curr->mmtid) $print_path: node '$label': $mode", $old);
}
}
else if ($curr) {
$this->assertTrue(TRUE, "($curr->mmtid) $print_path: node '$label'");
}
else {
$this->assertTrue(TRUE, "$print_path: node '$label'");
}
}
}
};
$test_page = function ($usr, $path, $nodes, $secondaries, &$stats) use ($test_node) {
$item = $path[count($path) - 1];
$print_path = [];
foreach ($path as $p) {
$print_path[] = $p->name;
}
$print_path = join('/', $print_path);
$new = mm_content_user_can($item->mmtid, '', $usr);
$modes = [
Constants::MM_PERMS_WRITE,
Constants::MM_PERMS_SUB,
Constants::MM_PERMS_APPLY,
Constants::MM_PERMS_READ,
Constants::MM_PERMS_IS_USER,
Constants::MM_PERMS_IS_GROUP,
Constants::MM_PERMS_IS_RECYCLE_BIN,
Constants::MM_PERMS_IS_RECYCLED,
];
foreach ($modes as $mode) {
if ($mode != Constants::MM_PERMS_APPLY || $path[0]->name != Constants::MM_ENTRY_NAME_GROUPS) {
$stats['count']++;
if (self::generateBaseline) {
$stats['baseline'][$print_path]['page'][$mode] = !empty($new[$mode]);
}
else if (!isset($this->baseline[$stats['label']][$print_path]['page'][$mode])) {
$stats['fail'][] = $this->failed("Undefined baseline entry ['" . $stats['label'] . "']['$print_path']['page'][$mode]. Re-run this test using PermissionsTest::wantBaseline=TRUE and use the output to rewrite " . self::baselineFilename . '.');
}
else {
$old = $this->baseline[$stats['label']][$print_path]['page'][$mode];
if ($new[$mode] !== $old) {
$stats['fail'][] = $this->failed("($item->mmtid) $print_path: $mode", $old);
}
else {
$this->assertTrue(TRUE, "($item->mmtid) $print_path: $mode");
}
}
}
}
/** @var $node Node */
foreach ($nodes as $node) {
$node->mm_catlist = [$item->mmtid => ''];
$node->save();
$test_node($usr, $print_path, $node, $stats, 0, $item, NULL);
$bin = mm_content_move_to_bin(NULL, $node->id());
mm_content_update_sort_queue();
$label = $node->label();
if (!is_numeric($bin)) {
$stats['fail'][] = $this->failed("Could not recycle the node '$label' at $print_path: $bin");
}
else {
$pp = $print_path . '/[recycled]';
$test_node($usr, $pp, $node, $stats, 0, NULL, NULL);
$err = mm_content_move_from_bin(NULL, $node, $bin, FALSE);
if (is_string($err)) {
$stats['fail'][] = $this->failed("Could not move '$label' out of recycle bin at $pp: $err");
}
else if ($secondaries) {
foreach ($secondaries as $pass => $secondary) {
$node->mm_catlist = [$item->mmtid => '', $secondary->mmtid => ''];
$node->save();
$test_node($usr, $print_path, $node, $stats, $pass + 1, $item, $secondary);
$bin = mm_content_move_to_bin(NULL, [$node->id() => [$item->mmtid]]);
mm_content_update_sort_queue();
if (!is_numeric($bin)) {
$stats['fail'][] = $this->failed("Could not recycle the node '$label' at $print_path: $bin");
}
else {
$test_node($usr, $pp, $node, $stats, $pass + 1, NULL, NULL);
$err = mm_content_move_from_bin(NULL, $node, $bin, FALSE);
if (is_string($err)) {
$stats['fail'][] = $this->failed("Could not move '$label' out of recycle bin at $pp: $err");
}
else {
$this->assertTrue(TRUE, "Moved '$label' out of recycle bin at $pp");
}
}
}
}
}
}
if ($nodes) {
// Flush the cache now instead of waiting until there are tons of entries
_mm_content_clear_access_cache();
}
};
$test_tree = function ($usr, $path, &$stats) {
$iter = new PermissionsTestIter([$path[0]->name, $path[1]->name], $stats, $this);
$params = [
Constants::MM_GET_TREE_ITERATOR => $iter,
Constants::MM_GET_TREE_FILTER_HIDDEN => TRUE,
Constants::MM_GET_TREE_RETURN_PERMS => TRUE,
Constants::MM_GET_TREE_USER => $usr,
];
mm_content_get_tree($path[1]->mmtid, $params);
};
/** @var AccountInterface $user */
$user = User::load(1);
if (!$user) {
throw new \Exception('Could not load the admin user.');
}
$this->container->set('current_user', $user);
$dummy_uid = 99999; // to keep mm_create_path() from using uid=1 by default
$roots = [mm_home_mmtid() => '[home]', mm_content_users_mmtid() => Constants::MM_ENTRY_NAME_USERS];
$stats = [];
foreach ($this->uids as $label => $test_uid) {
$stats[$test_uid] = ['label' => $label, 'count' => 0, 'fail' => [], 'baseline' => []];
/** @var AccountInterface $usr */
$usr = User::load($test_uid);
$grp = [
new MMCreatePathGroup([
'name' => Constants::MM_ENTRY_NAME_GROUPS,
'mmtid' => mm_content_groups_mmtid(),
]),
new MMCreatePathGroup([
'name' => '~MM TEST',
'members' => $test_uid ? [$test_uid] : [],
]),
];
if (!$this->mmCreatePath->createPath($grp)) {
throw new \Exception('Create group failed');
}
$gid = $grp[1]->mmtid;
if ($test_uid) {
$vgrp = [
&$grp[0],
new MMCreatePathGroup([
'name' => Constants::MM_ENTRY_NAME_VIRTUAL_GROUP,
]),
new MMCreatePathGroup([
'name' => '~MM TEST',
'vgroup' => TRUE,
'qfield' => $test_uid,
'qfrom' => '',
]),
];
if (!$this->mmCreatePath->createPath($vgrp)) {
throw new \Exception('Create vgroup failed');
}
$q = $database->select('mm_group');
$q->addField('mm_group','vgid');
$q->condition('gid', $vgrp[2]->mmtid);
$vgid = $q->execute()->fetchField();
$database->delete('mm_virtual_group')->condition('vgid', $vgid)->execute();
$database->insert('mm_virtual_group')
->fields(['vgid' => $vgid, 'uid' => $test_uid, 'preview' => 0])
->execute();
}
$nodes = [];
if ($this->testNodes) {
// Create a bunch of nodes, initially not on any page.
// This section requires $user->id() == 1.
if (!self::saveNode($user, [], [], $nodes, 'owned by admin') ||
!self::saveNode($usr, [], [], $nodes, 'owned by user') ||
!self::saveNode($user, [], [], $nodes, 'writable by everyone', TRUE) ||
$test_uid &&
(!self::saveNode($user, [$gid => ''], [], $nodes, 'writable by user in group') ||
!self::saveNode($user, [], [$test_uid => ''], $nodes, 'writable by user in ad hoc group') ||
!self::saveNode($user, [$vgid = ''], [], $nodes, 'writable by user in virtual group'))) {
return;
}
}
$parents = [
new MMCreatePathCat([
'name' => 'unreadable parent',
'alias' => 'xparent',
'default_mode' => '',
'uid' => $dummy_uid,
]),
new MMCreatePathCat([
'name' => 'parent readable by everyone',
'alias' => 'rparent',
'uid' => $dummy_uid,
]),
];
if ($test_uid) {
$parents[] = new MMCreatePathCat([
'name' => 'parent readable by regular group',
'alias' => 'rreggroupparent',
'perms' => [Constants::MM_PERMS_READ => ['groups' => [&$grp]]],
'uid' => $dummy_uid,
]);
$parents[] = new MMCreatePathCat([
'name' => 'parent readable by ad hoc group',
'alias' => 'radhocgroupparent',
'perms' => [Constants::MM_PERMS_READ => ['users' => [$test_uid]]],
'uid' => $dummy_uid,
]);
$parents[] = new MMCreatePathCat([
'name' => 'parent readable by virtual group',
'alias' => 'rvirtgroupparent',
'perms' => [Constants::MM_PERMS_READ => ['groups' => [&$vgrp]]],
'uid' => $dummy_uid,
]);
}
foreach ($roots as $root => $root_name) {
$this->deleteIfExists(['parent' => $root, 'alias' => '~mmtest']);
foreach ($parents as $parent) {
$path = [
new MMCreatePathCat([
'mmtid' => $root,
'name' => $root_name,
]),
new MMCreatePathCat([
'name' => '~MM TEST',
'alias' => '~mmtest',
'uid' => $dummy_uid,
]),
clone($parent),
new MMCreatePathCat([
'name' => 'no read',
'alias' => 'noread',
'default_mode' => '',
'uid' => $dummy_uid,
]),
];
$this->mmCreatePath->createPath($path);
// Test nodes first on the page by themselves, then also on a
// world-readable page and an unreadable page.
$secondaries = [$path[1], $path[3]];
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
$path[3] = new MMCreatePathCat([
'name' => 'owns',
'alias' => 'owns',
'default_mode' => '',
'uid' => $test_uid,
]);
$this->mmCreatePath->createPath($path);
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
$path[3] = new MMCreatePathCat([
'name' => 'inaccessible',
'alias' => 'inaccessible',
'default_mode' => '',
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($path);
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
foreach (['read', 'write', 'add sub', 'use'] as $long) {
$short = $long[0];
$path[3] = new MMCreatePathCat([
'name' => $long . ' by everyone',
'alias' => $short . 'everyone',
'default_mode' => $short,
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($path);
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
if ($test_uid) {
$path[3] = new MMCreatePathCat([
'name' => $long . ' by regular group',
'alias' => $short . 'reggroup',
'default_mode' => '',
'perms' => [$short => ['groups' => [&$grp]]],
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($path);
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
$path[3] = new MMCreatePathCat([
'name' => $long . ' by ad hoc group',
'alias' => $short . 'adhocgroup',
'default_mode' => '',
'perms' => [$short => ['users' => [$test_uid]]],
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($path);
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
$path[3] = new MMCreatePathCat([
'name' => $long . ' by virtual group',
'alias' => $short . 'virtgroup',
'default_mode' => '',
'perms' => [$short => ['groups' => [&$vgrp]]],
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($path);
$test_page($usr, $path, $nodes, $secondaries, $stats[$test_uid]);
}
}
}
// recycle bin
$bin = mm_content_move_to_bin($path[1]->mmtid);
mm_content_update_sort_queue();
if (!is_numeric($bin)) {
throw new \Exception("Error while moving '" . $path[1]->name . "' (" . $path[1]->mmtid . ") to recycle bin: $bin");
}
$path2 = $path;
$path2[1] = new MMCreatePathCat([
'name' => $path[1]->name . ' [recycled]',
'mmtid' => $path[1]->mmtid,
]);
unset($path2[3]);
$test_page($usr, $path2, [], [], $stats[$test_uid]); // subpage
unset($path2[2]);
$test_page($usr, $path2, [], [], $stats[$test_uid]); // page
$test_page($usr, [
new MMCreatePathCat([
'name' => $root_name . '/[recycle bin]',
'mmtid' => $bin,
])
], [], [], $stats[$test_uid]); // bin itself
$err = mm_content_move_from_bin($path2[1]->mmtid, NULL, NULL, FALSE);
mm_content_update_sort_queue();
if (is_string($err)) {
throw new \Exception("Error while moving '" . $path2[1]->name . "' (" . $path2[1]->mmtid . ") out of recycle bin: $err");
}
if (!self::generateBaseline) {
$test_tree($usr, [$path[0], $path[1]], $stats[$test_uid]);
}
}
// groups
$tgrp = [
&$grp[0],
new MMCreatePathGroup([
'name' => '~MM TEST',
]),
new MMCreatePathGroup([
'name' => 'owns',
'alias' => 'owns',
'default_mode' => '',
'uid' => $test_uid,
]),
];
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
$tgrp[2] = new MMCreatePathGroup([
'name' => 'inaccessible',
'alias' => 'inaccessible',
'default_mode' => Constants::MM_PERMS_APPLY,
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
$tgrp[3] = new MMCreatePathGroup([
'name' => 'read by everyone child of inaccessible',
'default_mode' => 'u,r',
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
unset($tgrp[3]);
foreach (['read', 'write', 'add sub'] as $long) {
$short = $long[0];
$tgrp[2] = new MMCreatePathGroup([
'name' => $long . ' by everyone',
'alias' => $short . 'everyone',
'default_mode' => "u,$short",
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
if ($test_uid) {
$tgrp[2] = new MMCreatePathGroup([
'name' => $long . ' by regular group',
'alias' => $short . 'reggroup',
'default_mode' => Constants::MM_PERMS_APPLY,
'perms' => [$short => ['groups' => [&$grp]]],
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
$tgrp[2] = new MMCreatePathGroup([
'name' => $long . ' by ad hoc group',
'alias' => $short . 'adhocgroup',
'default_mode' => Constants::MM_PERMS_APPLY,
'perms' => [$short => ['users' => [$test_uid]]],
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
$tgrp[2] = new MMCreatePathGroup([
'name' => $long . ' by virtual group',
'alias' => $short . 'virtgroup',
'default_mode' => Constants::MM_PERMS_APPLY,
'perms' => [$short => ['groups' => [&$vgrp]]],
'uid' => $dummy_uid,
]);
$this->mmCreatePath->createPath($tgrp);
$test_page($usr, $tgrp, [], [], $stats[$test_uid]);
}
}
if (!self::generateBaseline) {
mm_content_update_sort_queue();
$test_tree($usr, [$tgrp[0], $tgrp[1]], $stats[$test_uid]);
}
// various standard locations
$list = [
1 => '[root]',
mm_home_mmtid() => '[home]',
mm_content_users_mmtid() => Constants::MM_ENTRY_NAME_USERS,
mm_content_groups_mmtid() => Constants::MM_ENTRY_NAME_GROUPS,
-65 => Constants::MM_ENTRY_NAME_USERS . '/A',
1_234_567_890 => '[non-existent]'
];
foreach ($list as $tid => $label) {
$path = [
new MMCreatePathCat([
'mmtid' => $tid,
'name' => $label,
])
];
$test_page($usr, $path, [], [], $stats[$test_uid]);
}
if ($nodes) {
/** @var NodeInterface $node */
$node = Node::create(['type' => 'khsgkjhdsg']);
$node->setTitle('unsaved node');
$test_node($usr, '[unsaved node]', $node, $stats[$test_uid], 0, NULL, NULL);
}
$this->mmCreatePath->clearCaches();
// delete nodes, so they get re-created for next user
foreach ($nodes as $node) {
$node->delete();
}
}
if (self::generateBaseline) {
$baseline = [];
foreach ($stats as $stat) {
$baseline[$stat['label']] = $stat['baseline'];
}
if ($fp = gzopen('/tmp/' . self::baselineFilename, 'w')) {
gzputs($fp, Yaml::encode($baseline));
gzclose($fp);
fwrite(STDOUT, 'The baseline file was saved as /tmp/' . self::baselineFilename . "\n");
}
else {
die('Could not create ' . self::baselineFilename . "\n");
}
}
else {
foreach ($stats as $uid => $stat) {
if ($stat['fail']) {
fwrite(STDOUT, sprintf("uid = %s (%s): %d of %d tests failed\n", $uid < 0 ? 'other' : $uid, $stat['label'], count($stat['fail']), $stat['count']));
fwrite(STDOUT, join("\n", $stat['fail']) . "\n\n");
}
}
}
}
public function failed($msg, $truth = NULL) {
if (!is_null($truth)) {
$msg = ($truth ? 'T' : 'F') . ": $msg";
}
$this->assertTrue(FALSE, $msg);
return $msg;
}
/**
* Create a node belonging to a particular user.
*
* @param AccountInterface $usr
* The node's owner.
* @param $groups_w
* The node's group permissions.
* @param $users_w
* The node's user permissions.
* @param $nodes
* The list of all created nodes.
* @param $title
* The node's title.
* @param bool $everyone
* The world-writable flag.
* @return bool
* Success or failure.
* @throws \Exception
*/
private static function saveNode(AccountInterface $usr, $groups_w, $users_w, &$nodes, $title, $everyone = FALSE) {
$node = Node::create([
'type' => 'story',
'uid' => $usr->id(),
'name' => $usr->getAccountName(),
'title' => $title,
'body' => 'body',
'status' => 1,
'comment' => 0,
'mm_catlist_restricted' => [],
'mm_catlist' => [],
'owner' => $usr->id(),
'groups_w' => $groups_w,
'users_w' => $users_w,
'others_w' => $everyone,
'show_node_info' => 3,
'revision' => FALSE,
'mm_others_w_force' => TRUE, // always allow "writable by everyone"
]);
try {
$node->save();
}
catch (\Exception $e) {
print($e->getMessage());
return FALSE;
}
$nodes[$node->id()] = Node::load($node->id());
if (!$nodes[$node->id()]) {
throw new \Exception('Could not reload node');
}
return TRUE;
}
private function deleteIfExists($arr) {
if ($existing = mm_content_get($arr)) {
mm_content_delete($existing[0]->mmtid);
mm_content_update_sort_queue();
$this->mmCreatePath->clearCaches();
}
}
}