Added testing coverage for Bookshelves

Created modified TestResponse so we can use DOM operations in new
Testcases as we move away from the BrowserKit tests.
This commit is contained in:
Dan Brown 2018-09-21 15:15:16 +01:00
parent 8ff969dd17
commit b59e5942c8
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
8 changed files with 595 additions and 28 deletions

View File

@ -1288,6 +1288,7 @@ class EntityRepo
* Returns the number of books that had permissions updated.
* @param Bookshelf $bookshelf
* @return int
* @throws \Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{

View File

@ -49,6 +49,7 @@ return [
// 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',

View File

@ -0,0 +1,170 @@
<?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]);
}
}

View File

@ -1,6 +1,7 @@
<?php namespace Tests;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Entity;
use BookStack\User;
use BookStack\Repos\EntityRepo;
@ -34,6 +35,63 @@ class RestrictionsTest extends BrowserKitTest
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();
@ -325,6 +383,23 @@ class RestrictionsTest extends BrowserKitTest
->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();
@ -413,6 +488,44 @@ class RestrictionsTest extends BrowserKitTest
->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();

View File

@ -1,5 +1,6 @@
<?php namespace Tests;
use BookStack\Bookshelf;
use BookStack\Page;
use BookStack\Repos\PermissionsRepo;
use BookStack\Role;
@ -16,32 +17,6 @@ class RolesTest extends BrowserKitTest
$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');
@ -203,6 +178,90 @@ class RolesTest extends BrowserKitTest
}
}
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', [

View File

@ -1,9 +1,11 @@
<?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;
@ -69,6 +71,25 @@ trait SharedTestHelpers
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
@ -140,4 +161,30 @@ trait SharedTestHelpers
$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);
}
}

View File

@ -2,13 +2,13 @@
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
@ -18,11 +18,46 @@ abstract class TestCase extends BaseTestCase
/**
* 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);
}
}

141
tests/TestResponse.php Normal file
View File

@ -0,0 +1,141 @@
<?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})";
}
}