* Returns the number of books that had permissions updated.
* @param Bookshelf $bookshelf
* @return int
+ * @throws \Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{
// Entities
'entity_not_found' => 'Entity not found',
+ 'bookshelf_not_found' => 'Bookshelf not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',
--- /dev/null
+<?php namespace Tests;
+
+use BookStack\Book;
+use BookStack\Bookshelf;
+
+class BookShelfTest extends TestCase
+{
+
+ public function test_shelves_shows_in_header_if_have_view_permissions()
+ {
+ $viewer = $this->getViewer();
+ $resp = $this->actingAs($viewer)->get('/');
+ $resp->assertElementContains('header', 'Shelves');
+
+ $viewer->roles()->delete();
+ $this->giveUserPermissions($viewer);
+ $resp = $this->actingAs($viewer)->get('/');
+ $resp->assertElementNotContains('header', 'Shelves');
+
+ $this->giveUserPermissions($viewer, ['bookshelf-view-all']);
+ $resp = $this->actingAs($viewer)->get('/');
+ $resp->assertElementContains('header', 'Shelves');
+
+ $viewer->roles()->delete();
+ $this->giveUserPermissions($viewer, ['bookshelf-view-own']);
+ $resp = $this->actingAs($viewer)->get('/');
+ $resp->assertElementContains('header', 'Shelves');
+ }
+
+ public function test_shelves_page_contains_create_link()
+ {
+ $resp = $this->asEditor()->get('/shelves');
+ $resp->assertElementContains('a', 'Create New Shelf');
+ }
+
+ public function test_shelves_create()
+ {
+ $booksToInclude = Book::take(2)->get();
+ $shelfInfo = [
+ 'name' => 'My test book' . str_random(4),
+ 'description' => 'Test book description ' . str_random(10)
+ ];
+ $resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
+ 'books' => $booksToInclude->implode('id', ','),
+ 'tags' => [
+ [
+ 'name' => 'Test Category',
+ 'value' => 'Test Tag Value',
+ ]
+ ],
+ ]));
+ $resp->assertRedirect();
+ $editorId = $this->getEditor()->id;
+ $this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['created_by' => $editorId, 'updated_by' => $editorId]));
+
+ $shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first();
+ $shelfPage = $this->get($shelf->getUrl());
+ $shelfPage->assertSee($shelfInfo['name']);
+ $shelfPage->assertSee($shelfInfo['description']);
+ $shelfPage->assertElementContains('.tag-item', 'Test Category');
+ $shelfPage->assertElementContains('.tag-item', 'Test Tag Value');
+
+ $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[0]->id]);
+ $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);
+ }
+
+ public function test_shelf_view()
+ {
+ $shelf = Bookshelf::first();
+ $resp = $this->asEditor()->get($shelf->getUrl());
+ $resp->assertStatus(200);
+ $resp->assertSeeText($shelf->name);
+ $resp->assertSeeText($shelf->description);
+
+ foreach ($shelf->books as $book) {
+ $resp->assertSee($book->name);
+ }
+ }
+
+ public function test_shelf_view_shows_action_buttons()
+ {
+ $shelf = Bookshelf::first();
+ $resp = $this->asAdmin()->get($shelf->getUrl());
+ $resp->assertSee($shelf->getUrl('/edit'));
+ $resp->assertSee($shelf->getUrl('/permissions'));
+ $resp->assertSee($shelf->getUrl('/delete'));
+ $resp->assertElementContains('a', 'Edit');
+ $resp->assertElementContains('a', 'Permissions');
+ $resp->assertElementContains('a', 'Delete');
+
+ $resp = $this->asEditor()->get($shelf->getUrl());
+ $resp->assertDontSee($shelf->getUrl('/permissions'));
+ }
+
+ public function test_shelf_edit()
+ {
+ $shelf = Bookshelf::first();
+ $resp = $this->asEditor()->get($shelf->getUrl('/edit'));
+ $resp->assertSeeText('Edit Bookshelf');
+
+ $booksToInclude = Book::take(2)->get();
+ $shelfInfo = [
+ 'name' => 'My test book' . str_random(4),
+ 'description' => 'Test book description ' . str_random(10)
+ ];
+
+ $resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
+ 'books' => $booksToInclude->implode('id', ','),
+ 'tags' => [
+ [
+ 'name' => 'Test Category',
+ 'value' => 'Test Tag Value',
+ ]
+ ],
+ ]));
+ $shelf = Bookshelf::find($shelf->id);
+ $resp->assertRedirect($shelf->getUrl());
+ $this->assertSessionHas('success');
+
+ $editorId = $this->getEditor()->id;
+ $this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['id' => $shelf->id, 'created_by' => $editorId, 'updated_by' => $editorId]));
+
+ $shelfPage = $this->get($shelf->getUrl());
+ $shelfPage->assertSee($shelfInfo['name']);
+ $shelfPage->assertSee($shelfInfo['description']);
+ $shelfPage->assertElementContains('.tag-item', 'Test Category');
+ $shelfPage->assertElementContains('.tag-item', 'Test Tag Value');
+
+ $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[0]->id]);
+ $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]);
+ }
+
+ public function test_shelf_delete()
+ {
+ $shelf = Bookshelf::first();
+ $resp = $this->asEditor()->get($shelf->getUrl('/delete'));
+ $resp->assertSeeText('Delete Bookshelf');
+ $resp->assertSee("action=\"{$shelf->getUrl()}\"");
+
+ $resp = $this->delete($shelf->getUrl());
+ $resp->assertRedirect('/shelves');
+ $this->assertDatabaseMissing('bookshelves', ['id' => $shelf->id]);
+ $this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]);
+ $this->assertSessionHas('success');
+ }
+
+ public function test_shelf_copy_permissions()
+ {
+ $shelf = Bookshelf::first();
+ $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));
+ $resp->assertSeeText('Copy Permissions');
+ $resp->assertSee("action=\"{$shelf->getUrl('/copy-permissions')}\"");
+
+ $child = $shelf->books()->first();
+ $editorRole = $this->getEditor()->roles()->first();
+ $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default");
+ $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default");
+
+ $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]);
+ $resp = $this->post($shelf->getUrl('/copy-permissions'));
+ $child = $shelf->books()->first();
+
+ $resp->assertRedirect($shelf->getUrl());
+ $this->assertTrue(boolval($child->restricted), "Child book should now be restricted");
+ $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions");
+ $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
+ $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
+ }
+
+}
<?php namespace Tests;
use BookStack\Book;
+use BookStack\Bookshelf;
use BookStack\Entity;
use BookStack\User;
use BookStack\Repos\EntityRepo;
parent::setEntityRestrictions($entity, $actions, $roles);
}
+ public function test_bookshelf_view_restriction()
+ {
+ $shelf = Bookshelf::first();
+
+ $this->actingAs($this->user)
+ ->visit($shelf->getUrl())
+ ->seePageIs($shelf->getUrl());
+
+ $this->setEntityRestrictions($shelf, []);
+
+ $this->forceVisit($shelf->getUrl())
+ ->see('Bookshelf not found');
+
+ $this->setEntityRestrictions($shelf, ['view']);
+
+ $this->visit($shelf->getUrl())
+ ->see($shelf->name);
+ }
+
+ public function test_bookshelf_update_restriction()
+ {
+ $shelf = BookShelf::first();
+
+ $this->actingAs($this->user)
+ ->visit($shelf->getUrl('/edit'))
+ ->see('Edit Book');
+
+ $this->setEntityRestrictions($shelf, ['view', 'delete']);
+
+ $this->forceVisit($shelf->getUrl('/edit'))
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($shelf, ['view', 'update']);
+
+ $this->visit($shelf->getUrl('/edit'))
+ ->seePageIs($shelf->getUrl('/edit'));
+ }
+
+ public function test_bookshelf_delete_restriction()
+ {
+ $shelf = Book::first();
+
+ $this->actingAs($this->user)
+ ->visit($shelf->getUrl('/delete'))
+ ->see('Delete Book');
+
+ $this->setEntityRestrictions($shelf, ['view', 'update']);
+
+ $this->forceVisit($shelf->getUrl('/delete'))
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($shelf, ['view', 'delete']);
+
+ $this->visit($shelf->getUrl('/delete'))
+ ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book');
+ }
+
public function test_book_view_restriction()
{
$book = Book::first();
->seePageIs($pageUrl . '/delete')->see('Delete Page');
}
+ public function test_bookshelf_restriction_form()
+ {
+ $shelf = Bookshelf::first();
+ $this->asAdmin()->visit($shelf->getUrl('/permissions'))
+ ->see('Bookshelf Permissions')
+ ->check('restricted')
+ ->check('restrictions[2][view]')
+ ->press('Save Permissions')
+ ->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true])
+ ->seeInDatabase('entity_permissions', [
+ 'restrictable_id' => $shelf->id,
+ 'restrictable_type' => 'BookStack\Bookshelf',
+ 'role_id' => '2',
+ 'action' => 'view'
+ ]);
+ }
+
public function test_book_restriction_form()
{
$book = Book::first();
->dontSee($page->name);
}
+ public function test_bookshelf_update_restriction_override()
+ {
+ $shelf = Bookshelf::first();
+
+ $this->actingAs($this->viewer)
+ ->visit($shelf->getUrl('/edit'))
+ ->dontSee('Edit Book');
+
+ $this->setEntityRestrictions($shelf, ['view', 'delete']);
+
+ $this->forceVisit($shelf->getUrl('/edit'))
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($shelf, ['view', 'update']);
+
+ $this->visit($shelf->getUrl('/edit'))
+ ->seePageIs($shelf->getUrl('/edit'));
+ }
+
+ public function test_bookshelf_delete_restriction_override()
+ {
+ $shelf = Bookshelf::first();
+
+ $this->actingAs($this->viewer)
+ ->visit($shelf->getUrl('/delete'))
+ ->dontSee('Delete Book');
+
+ $this->setEntityRestrictions($shelf, ['view', 'update']);
+
+ $this->forceVisit($shelf->getUrl('/delete'))
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($shelf, ['view', 'delete']);
+
+ $this->visit($shelf->getUrl('/delete'))
+ ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book');
+ }
+
public function test_book_create_restriction_override()
{
$book = Book::first();
<?php namespace Tests;
+use BookStack\Bookshelf;
use BookStack\Page;
use BookStack\Repos\PermissionsRepo;
use BookStack\Role;
$this->user = $this->getViewer();
}
- /**
- * Give the given user some permissions.
- * @param \BookStack\User $user
- * @param array $permissions
- */
- protected function giveUserPermissions(\BookStack\User $user, $permissions = [])
- {
- $newRole = $this->createNewRole($permissions);
- $user->attachRole($newRole);
- $user->load('roles');
- $user->permissions(false);
- }
-
- /**
- * Create a new basic role for testing purposes.
- * @param array $permissions
- * @return Role
- */
- protected function createNewRole($permissions = [])
- {
- $permissionRepo = app(PermissionsRepo::class);
- $roleData = factory(\BookStack\Role::class)->make()->toArray();
- $roleData['permissions'] = array_flip($permissions);
- return $permissionRepo->saveNewRole($roleData);
- }
-
public function test_admin_can_see_settings()
{
$this->asAdmin()->visit('/settings')->see('Settings');
}
}
+ public function test_bookshelves_create_all_permissions()
+ {
+ $this->checkAccessPermission('bookshelf-create-all', [
+ '/create-shelf'
+ ], [
+ '/shelves' => 'Create New Shelf'
+ ]);
+
+ $this->visit('/create-shelf')
+ ->type('test shelf', 'name')
+ ->type('shelf desc', 'description')
+ ->press('Save Shelf')
+ ->seePageIs('/shelves/test-shelf');
+ }
+
+ public function test_bookshelves_edit_own_permission()
+ {
+ $otherShelf = Bookshelf::first();
+ $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
+ $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+ $this->regenEntityPermissions($ownShelf);
+
+ $this->checkAccessPermission('bookshelf-update-own', [
+ $ownShelf->getUrl('/edit')
+ ], [
+ $ownShelf->getUrl() => 'Edit'
+ ]);
+
+ $this->visit($otherShelf->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Edit')
+ ->visit($otherShelf->getUrl('/edit'))
+ ->seePageIs('/');
+ }
+
+ public function test_bookshelves_edit_all_permission()
+ {
+ $otherShelf = \BookStack\Bookshelf::first();
+ $this->checkAccessPermission('bookshelf-update-all', [
+ $otherShelf->getUrl('/edit')
+ ], [
+ $otherShelf->getUrl() => 'Edit'
+ ]);
+ }
+
+ public function test_bookshelves_delete_own_permission()
+ {
+ $this->giveUserPermissions($this->user, ['bookshelf-update-all']);
+ $otherShelf = \BookStack\Bookshelf::first();
+ $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
+ $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+ $this->regenEntityPermissions($ownShelf);
+
+ $this->checkAccessPermission('bookshelf-delete-own', [
+ $ownShelf->getUrl('/delete')
+ ], [
+ $ownShelf->getUrl() => 'Delete'
+ ]);
+
+ $this->visit($otherShelf->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Delete')
+ ->visit($otherShelf->getUrl('/delete'))
+ ->seePageIs('/');
+ $this->visit($ownShelf->getUrl())->visit($ownShelf->getUrl('/delete'))
+ ->press('Confirm')
+ ->seePageIs('/shelves')
+ ->dontSee($ownShelf->name);
+ }
+
+ public function test_bookshelves_delete_all_permission()
+ {
+ $this->giveUserPermissions($this->user, ['bookshelf-update-all']);
+ $otherShelf = \BookStack\Bookshelf::first();
+ $this->checkAccessPermission('bookshelf-delete-all', [
+ $otherShelf->getUrl('/delete')
+ ], [
+ $otherShelf->getUrl() => 'Delete'
+ ]);
+
+ $this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete'))
+ ->press('Confirm')
+ ->seePageIs('/shelves')
+ ->dontSee($otherShelf->name);
+ }
+
public function test_books_create_all_permissions()
{
$this->checkAccessPermission('book-create-all', [
<?php namespace Tests;
use BookStack\Book;
+use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Repos\EntityRepo;
+use BookStack\Repos\PermissionsRepo;
use BookStack\Role;
use BookStack\Services\PermissionService;
use BookStack\Services\SettingService;
return $user;
}
+ /**
+ * Regenerate the permission for an entity.
+ * @param Entity $entity
+ */
+ protected function regenEntityPermissions(Entity $entity)
+ {
+ $this->app[PermissionService::class]->buildJointPermissionsForEntity($entity);
+ $entity->load('jointPermissions');
+ }
+
+ /**
+ * Create and return a new bookshelf.
+ * @param array $input
+ * @return Bookshelf
+ */
+ public function newShelf($input = ['name' => 'test shelf', 'description' => 'My new test shelf']) {
+ return $this->app[EntityRepo::class]->createFromInput('bookshelf', $input, false);
+ }
+
/**
* Create and return a new book.
* @param array $input
$entity->load('jointPermissions');
}
+ /**
+ * Give the given user some permissions.
+ * @param \BookStack\User $user
+ * @param array $permissions
+ */
+ protected function giveUserPermissions(\BookStack\User $user, $permissions = [])
+ {
+ $newRole = $this->createNewRole($permissions);
+ $user->attachRole($newRole);
+ $user->load('roles');
+ $user->permissions(false);
+ }
+
+ /**
+ * Create a new basic role for testing purposes.
+ * @param array $permissions
+ * @return Role
+ */
+ protected function createNewRole($permissions = [])
+ {
+ $permissionRepo = app(PermissionsRepo::class);
+ $roleData = factory(Role::class)->make()->toArray();
+ $roleData['permissions'] = array_flip($permissions);
+ return $permissionRepo->saveNewRole($roleData);
+ }
+
}
\ No newline at end of file
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
-use Illuminate\Foundation\Testing\TestResponse;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use DatabaseTransactions;
use SharedTestHelpers;
+
/**
* The base URL to use while testing the application.
* @var string
/**
* Assert a permission error has occurred.
* @param TestResponse $response
+ * @return TestCase
*/
protected function assertPermissionError(TestResponse $response)
{
$response->assertRedirect('/');
- $this->assertTrue(session()->has('error'));
+ $this->assertSessionHas('error');
session()->remove('error');
+ return $this;
+ }
+
+ /**
+ * Assert the session contains a specific entry.
+ * @param string $key
+ * @return $this
+ */
+ protected function assertSessionHas(string $key)
+ {
+ $this->assertTrue(session()->has($key), "Session does not contain a [{$key}] entry");
+ return $this;
+ }
+
+ /**
+ * Override of the get method so we can get visibility of custom TestResponse methods.
+ * @param string $uri
+ * @param array $headers
+ * @return TestResponse
+ */
+ public function get($uri, array $headers = [])
+ {
+ return parent::get($uri, $headers);
+ }
+
+ /**
+ * Create the test response instance from the given response.
+ *
+ * @param \Illuminate\Http\Response $response
+ * @return TestResponse
+ */
+ protected function createTestResponse($response)
+ {
+ return TestResponse::fromBaseResponse($response);
}
}
\ No newline at end of file
--- /dev/null
+<?php namespace Tests;
+
+use \Illuminate\Foundation\Testing\TestResponse as BaseTestResponse;
+use Symfony\Component\DomCrawler\Crawler;
+use PHPUnit\Framework\Assert as PHPUnit;
+
+/**
+ * Class TestResponse
+ * Custom extension of the default Laravel TestResponse class.
+ * @package Tests
+ */
+class TestResponse extends BaseTestResponse {
+
+ protected $crawlerInstance;
+
+ /**
+ * Get the DOM Crawler for the response content.
+ * @return Crawler
+ */
+ protected function crawler()
+ {
+ if (!is_object($this->crawlerInstance)) {
+ $this->crawlerInstance = new Crawler($this->getContent());
+ }
+ return $this->crawlerInstance;
+ }
+
+ /**
+ * Assert the response contains the specified element.
+ * @param string $selector
+ * @return $this
+ */
+ public function assertElementExists(string $selector)
+ {
+ $elements = $this->crawler()->filter($selector);
+ PHPUnit::assertTrue(
+ $elements->count() > 0,
+ 'Unable to find element matching the selector: '.PHP_EOL.PHP_EOL.
+ "[{$selector}]".PHP_EOL.PHP_EOL.
+ 'within'.PHP_EOL.PHP_EOL.
+ "[{$this->getContent()}]."
+ );
+ return $this;
+ }
+
+ /**
+ * Assert the response does not contain the specified element.
+ * @param string $selector
+ * @return $this
+ */
+ public function assertElementNotExists(string $selector)
+ {
+ $elements = $this->crawler()->filter($selector);
+ PHPUnit::assertTrue(
+ $elements->count() === 0,
+ 'Found elements matching the selector: '.PHP_EOL.PHP_EOL.
+ "[{$selector}]".PHP_EOL.PHP_EOL.
+ 'within'.PHP_EOL.PHP_EOL.
+ "[{$this->getContent()}]."
+ );
+ return $this;
+ }
+
+ /**
+ * Assert the response includes a specific element containing the given text.
+ * @param string $selector
+ * @param string $text
+ * @return $this
+ */
+ public function assertElementContains(string $selector, string $text)
+ {
+ $elements = $this->crawler()->filter($selector);
+ $matched = false;
+ $pattern = $this->getEscapedPattern($text);
+ foreach ($elements as $element) {
+ $element = new Crawler($element);
+ if (preg_match("/$pattern/i", $element->html())) {
+ $matched = true;
+ break;
+ }
+ }
+
+ PHPUnit::assertTrue(
+ $matched,
+ 'Unable to find element of selector: '.PHP_EOL.PHP_EOL.
+ "[{$selector}]".PHP_EOL.PHP_EOL.
+ 'containing text'.PHP_EOL.PHP_EOL.
+ "[{$text}]".PHP_EOL.PHP_EOL.
+ 'within'.PHP_EOL.PHP_EOL.
+ "[{$this->getContent()}]."
+ );
+
+ return $this;
+ }
+
+ /**
+ * Assert the response does not include a specific element containing the given text.
+ * @param string $selector
+ * @param string $text
+ * @return $this
+ */
+ public function assertElementNotContains(string $selector, string $text)
+ {
+ $elements = $this->crawler()->filter($selector);
+ $matched = false;
+ $pattern = $this->getEscapedPattern($text);
+ foreach ($elements as $element) {
+ $element = new Crawler($element);
+ if (preg_match("/$pattern/i", $element->html())) {
+ $matched = true;
+ break;
+ }
+ }
+
+ PHPUnit::assertTrue(
+ !$matched,
+ 'Found element of selector: '.PHP_EOL.PHP_EOL.
+ "[{$selector}]".PHP_EOL.PHP_EOL.
+ 'containing text'.PHP_EOL.PHP_EOL.
+ "[{$text}]".PHP_EOL.PHP_EOL.
+ 'within'.PHP_EOL.PHP_EOL.
+ "[{$this->getContent()}]."
+ );
+
+ return $this;
+ }
+
+ /**
+ * Get the escaped text pattern for the constraint.
+ * @param string $text
+ * @return string
+ */
+ protected function getEscapedPattern($text)
+ {
+ $rawPattern = preg_quote($text, '/');
+ $escapedPattern = preg_quote(e($text), '/');
+ return $rawPattern == $escapedPattern
+ ? $rawPattern : "({$rawPattern}|{$escapedPattern})";
+ }
+
+}