Added restriction tests and fixed any bugs in the process

Also updated many styles within areas affected by the new permission and roles system.
This commit is contained in:
Dan Brown 2016-03-05 18:09:21 +00:00
parent 268db6b1d0
commit 8e6248f57f
26 changed files with 680 additions and 32 deletions

View File

@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
// Which will include the basic message to point the user roughly to the cause.
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
return response()->view('errors/500', ['message' => $message], 500);
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
return response()->view('errors/' . $code, ['message' => $message], $code);
}
return parent::render($request, $e);

View File

@ -0,0 +1,14 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
/**
* NotFoundException constructor.
* @param string $message
*/
public function __construct($message = 'Item not found')
{
parent::__construct($message, 404);
}
}

View File

@ -80,7 +80,14 @@ class ChapterController extends Controller
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
$pages = $this->chapterRepo->getChildren($chapter);
return view('chapters/show', [
'book' => $book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
]);
}
/**

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
@ -94,7 +95,7 @@ class PageController extends Controller
try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
} catch (NotFoundHttpException $e) {
} catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
return redirect($page->getUrl());

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService;
use Illuminate\Support\Str;
use BookStack\Book;
@ -111,11 +112,12 @@ class BookRepo
* Get a book by slug
* @param $slug
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug)
{
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) abort(404);
if ($book === null) throw new NotFoundException('Book not found');
return $book;
}
@ -153,6 +155,7 @@ class BookRepo
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
$book->restrictions()->delete();
$book->delete();
}
@ -210,11 +213,13 @@ class BookRepo
public function getChildren(Book $book)
{
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
$this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
$pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with('pages');
$this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapterQuery = $book->chapters()->with(['pages' => function($query) {
$this->restrictionService->enforcePageRestrictions($query, 'view');
}]);
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
$children = $pages->merge($chapters);
$bookSlug = $book->slug;

View File

@ -2,6 +2,7 @@
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService;
use Illuminate\Support\Str;
use BookStack\Chapter;
@ -66,14 +67,24 @@ class ChapterRepo
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) abort(404);
if ($chapter === null) throw new NotFoundException('Chapter not found');
return $chapter;
}
/**
* Get the child items for a chapter
* @param Chapter $chapter
*/
public function getChildren(Chapter $chapter)
{
return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
}
/**
* Create a new chapter from request input.
* @param $input
@ -98,6 +109,7 @@ class ChapterRepo
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->restrictions()->delete();
$chapter->delete();
}

View File

@ -4,6 +4,7 @@
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -56,11 +57,12 @@ class PageRepo
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) throw new NotFoundHttpException('Page not found');
if ($page === null) throw new NotFoundException('Page not found');
return $page;
}
@ -373,6 +375,7 @@ class PageRepo
Activity::removeEntity($page);
$page->views()->delete();
$page->revisions()->delete();
$page->restrictions()->delete();
$page->delete();
}

View File

@ -15,10 +15,16 @@ class RestrictionService
public function __construct()
{
$user = auth()->user();
$this->userRoles = $user ? auth()->user()->roles->pluck('id') : false;
$this->userRoles = $user ? auth()->user()->roles->pluck('id') : [];
$this->isAdmin = $user ? auth()->user()->hasRole('admin') : false;
}
/**
* Checks if an entity has a restriction set upon it.
* @param Entity $entity
* @param $action
* @return bool
*/
public function checkIfEntityRestricted(Entity $entity, $action)
{
if ($this->isAdmin) return true;
@ -93,12 +99,28 @@ class RestrictionService
});
});
})
// Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
})
->whereExists(function ($query) {
$query->select('*')->from('books')
->whereRaw('books.id=pages.book_id')
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
})
// Page unrestricted, Has a chapter with accepted permissions
->orWhere(function ($query) {
$query->where('restricted', '=', false)
->whereExists(function ($query) {
$query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id')
->where('restricted', '=', true)
->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
});
@ -183,10 +205,12 @@ class RestrictionService
return $query->where(function ($parentWhereQuery) {
$parentWhereQuery
->where('restricted', '=', false)
->orWhereExists(function ($query) {
->orWhere(function ($query) {
$query->where('restricted', '=', true)->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book');
});
});
});
}
/**

View File

@ -39,6 +39,7 @@ if (! function_exists('versioned_asset')) {
*/
function userCan($permission, \BookStack\Ownable $ownable = null)
{
if (!auth()->check()) return false;
if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission);
}
@ -47,7 +48,7 @@ function userCan($permission, \BookStack\Ownable $ownable = null)
$permissionBaseName = strtolower($permission) . '-';
$hasPermission = false;
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;

View File

@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
$role = \BookStack\Role::getDefault();
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);

View File

@ -21,6 +21,7 @@
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>

View File

@ -87,6 +87,9 @@ header {
padding-top: $-s;
}
}
.dropdown-container {
font-size: 0.9em;
}
}
form.search-box {

View File

@ -95,13 +95,14 @@
// Sidebar list
.book-tree {
padding: $-xl 0 0 0;
padding: $-l 0 0 0;
position: relative;
right: 0;
top: 0;
transition: ease-in-out 240ms;
transition-property: right, border;
border-left: 0px solid #FFF;
background-color: #FFF;
&.fixed {
position: fixed;
top: 0;

View File

@ -30,8 +30,10 @@
{!! $books->render() !!}
@else
<p class="text-muted">No books have been created.</p>
@if(userCan('books-create-all'))
<a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
@endif
@endif
</div>
<div class="col-sm-4 col-sm-offset-1">
<div id="recents">

View File

@ -2,6 +2,19 @@
@section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable>
<h1>Book Restrictions</h1>
@include('form/restriction-form', ['model' => $book])

View File

@ -2,7 +2,7 @@
@section('content')
<div class="faded-small toolbar" ng-non-bindable>
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-md-12">
@ -15,13 +15,22 @@
@endif
@if(userCan('book-update', $book))
<a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
<a href="{{ $book->getUrl() }}/sort" class="text-primary text-button"><i class="zmdi zmdi-sort"></i>Sort</a>
@endif
@if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
<div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
<ul>
@if(userCan('book-update', $book))
<li><a href="{{ $book->getUrl() }}/sort" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li>
@endif
@if(userCan('restrictions-manage', $book))
<a href="{{$book->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
<li><a href="{{$book->getUrl()}}/restrict" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Restrict</a></li>
@endif
@if(userCan('book-delete', $book))
<a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
<li><a href="{{ $book->getUrl() }}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
@endif
</ul>
</div>
@endif
</div>
</div>
@ -78,6 +87,15 @@
<div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div>
@if($book->restricted)
<p class="text-muted">
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
@endif
</p>
@endif
<div class="search-box">
<form ng-submit="searchBook($event)">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">

View File

@ -2,6 +2,20 @@
@section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$chapter->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span>
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable>
<h1>Chapter Restrictions</h1>
@include('form/restriction-form', ['model' => $chapter])

View File

@ -37,10 +37,10 @@
<h1>{{ $chapter->name }}</h1>
<p class="text-muted">{{ $chapter->description }}</p>
@if(count($chapter->pages) > 0)
@if(count($pages) > 0)
<div class="page-list">
<hr>
@foreach($chapter->pages as $page)
@foreach($pages as $page)
@include('pages/list-item', ['page' => $page])
<hr>
@endforeach
@ -63,6 +63,29 @@
</p>
</div>
<div class="col-md-3 col-md-offset-1">
<div class="margin-top large"></div>
@if($book->restricted || $chapter->restricted)
<div class="text-muted">
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
@endif
<br>
@endif
@if($chapter->restricted)
@if(userCan('restrictions-manage', $chapter))
<a href="{{ $chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter Restricted
@endif
@endif
</div>
@endif
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div>
</div>

View File

@ -4,7 +4,7 @@
<div class="container">
<h1 class="text-muted">Page Not Found</h1>
<h1 class="text-muted">{{ $message or 'Page Not Found' }}</h1>
<p>Sorry, The page you were looking for could not be found.</p>
<a href="/" class="button">Return To Home</a>
</div>

View File

@ -3,7 +3,7 @@
<input type="hidden" name="_method" value="PUT">
<div class="form-group">
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this page?'])
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
</div>
<table class="table">
@ -24,5 +24,6 @@
@endforeach
</table>
<a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Restrictions</button>
</form>

View File

@ -2,6 +2,27 @@
@section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$page->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
@if($page->hasChapter())
<span class="sep">&raquo;</span>
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
<i class="zmdi zmdi-collection-bookmark"></i>
{{$page->chapter->getShortName()}}
</a>
@endif
<span class="sep">&raquo;</span>
<a href="{{$page->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable>
<h1>Page Restrictions</h1>
@include('form/restriction-form', ['model' => $page])

View File

@ -70,7 +70,38 @@
</div>
</div>
<div class="col-md-3 print-hidden">
<div class="margin-top large"></div>
@if($book->restricted || ($page->chapter && $page->chapter->restricted) || $page->restricted)
<div class="text-muted">
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book restricted
@endif
<br>
@endif
@if($page->chapter && $page->chapter->restricted)
@if(userCan('restrictions-manage', $page->chapter))
<a href="{{ $page->chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter restricted
@endif
<br>
@endif
@if($page->restricted)
@if(userCan('restrictions-manage', $page))
<a href="{{ $page->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Page restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Page restricted
@endif
<br>
@endif
</div>
@endif
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div>

View File

@ -3,6 +3,7 @@
<div class="row">
<div class="col-md-6">
<h3>Role Details</h3>
<div class="form-group">
<label for="name">Role Name</label>
@include('form/text', ['name' => 'display_name'])
@ -11,7 +12,7 @@
<label for="name">Short Role Description</label>
@include('form/text', ['name' => 'description'])
</div>
<hr class="even">
<h3>System Permissions</h3>
<div class="row">
<div class="col-md-6">
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
@ -33,10 +34,17 @@
<div class="form-group">
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
</div>
<hr class="even">
</div>
<div class="col-md-6">
<h3>Asset Permissions</h3>
<p>
These permissions control default access to the assets within the system. <br>
Restrictions on Books, Chapters and Pages will override these permissions.
</p>
<table class="table">
<tr>
<th></th>
@ -104,4 +112,6 @@
</div>
</div>
<a href="/settings/roles" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Role</button>

View File

@ -4,7 +4,7 @@
@include('settings/navbar', ['selected' => 'roles'])
<div class="container">
<div class="container small">
<h1>User Roles</h1>

407
tests/RestrictionsTest.php Normal file
View File

@ -0,0 +1,407 @@
<?php
class RestrictionsTest extends TestCase
{
protected $user;
public function setUp()
{
parent::setUp();
$this->user = $this->getNewUser();
}
/**
* Manually set some restrictions on an entity.
* @param \BookStack\Entity $entity
* @param $actions
*/
protected function setEntityRestrictions(\BookStack\Entity $entity, $actions)
{
$entity->restricted = true;
$entity->restrictions()->delete();
$role = $this->user->roles->first();
foreach ($actions as $action) {
$entity->restrictions()->create([
'role_id' => $role->id,
'action' => strtolower($action)
]);
}
$entity->save();
$entity->load('restrictions');
}
public function test_book_view_restriction()
{
$book = \BookStack\Book::first();
$bookPage = $book->pages->first();
$bookChapter = $book->chapters->first();
$bookUrl = $book->getUrl();
$this->actingAs($this->user)
->visit($bookUrl)
->seePageIs($bookUrl);
$this->setEntityRestrictions($book, []);
$this->forceVisit($bookUrl)
->see('Book not found');
$this->forceVisit($bookPage->getUrl())
->see('Book not found');
$this->forceVisit($bookChapter->getUrl())
->see('Book not found');
$this->setEntityRestrictions($book, ['view']);
$this->visit($bookUrl)
->see($book->name);
$this->visit($bookPage->getUrl())
->see($bookPage->name);
$this->visit($bookChapter->getUrl())
->see($bookChapter->name);
}
public function test_book_create_restriction()
{
$book = \BookStack\Book::first();
$bookUrl = $book->getUrl();
$this->actingAs($this->user)
->visit($bookUrl)
->seeInElement('.action-buttons', 'New Page')
->seeInElement('.action-buttons', 'New Chapter');
$this->setEntityRestrictions($book, ['view', 'delete', 'update']);
$this->forceVisit($bookUrl . '/chapter/create')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookUrl . '/page/create')
->see('You do not have permission')->seePageIs('/');
$this->visit($bookUrl)->dontSeeInElement('.action-buttons', 'New Page')
->dontSeeInElement('.action-buttons', 'New Chapter');
$this->setEntityRestrictions($book, ['view', 'create']);
$this->visit($bookUrl . '/chapter/create')
->type('test chapter', 'name')
->type('test description for chapter', 'description')
->press('Save Chapter')
->seePageIs($bookUrl . '/chapter/test-chapter');
$this->visit($bookUrl . '/page/create')
->type('test page', 'name')
->type('test content', 'html')
->press('Save Page')
->seePageIs($bookUrl . '/page/test-page');
$this->visit($bookUrl)->seeInElement('.action-buttons', 'New Page')
->seeInElement('.action-buttons', 'New Chapter');
}
public function test_book_update_restriction()
{
$book = \BookStack\Book::first();
$bookPage = $book->pages->first();
$bookChapter = $book->chapters->first();
$bookUrl = $book->getUrl();
$this->actingAs($this->user)
->visit($bookUrl . '/edit')
->see('Edit Book');
$this->setEntityRestrictions($book, ['view', 'delete']);
$this->forceVisit($bookUrl . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookPage->getUrl() . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookChapter->getUrl() . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($book, ['view', 'update']);
$this->visit($bookUrl . '/edit')
->seePageIs($bookUrl . '/edit');
$this->visit($bookPage->getUrl() . '/edit')
->seePageIs($bookPage->getUrl() . '/edit');
$this->visit($bookChapter->getUrl() . '/edit')
->see('Edit Chapter');
}
public function test_book_delete_restriction()
{
$book = \BookStack\Book::first();
$bookPage = $book->pages->first();
$bookChapter = $book->chapters->first();
$bookUrl = $book->getUrl();
$this->actingAs($this->user)
->visit($bookUrl . '/delete')
->see('Delete Book');
$this->setEntityRestrictions($book, ['view', 'update']);
$this->forceVisit($bookUrl . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookPage->getUrl() . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookChapter->getUrl() . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($book, ['view', 'delete']);
$this->visit($bookUrl . '/delete')
->seePageIs($bookUrl . '/delete')->see('Delete Book');
$this->visit($bookPage->getUrl() . '/delete')
->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page');
$this->visit($bookChapter->getUrl() . '/delete')
->see('Delete Chapter');
}
public function test_chapter_view_restriction()
{
$chapter = \BookStack\Chapter::first();
$chapterPage = $chapter->pages->first();
$chapterUrl = $chapter->getUrl();
$this->actingAs($this->user)
->visit($chapterUrl)
->seePageIs($chapterUrl);
$this->setEntityRestrictions($chapter, []);
$this->forceVisit($chapterUrl)
->see('Chapter not found');
$this->forceVisit($chapterPage->getUrl())
->see('Page not found');
$this->setEntityRestrictions($chapter, ['view']);
$this->visit($chapterUrl)
->see($chapter->name);
$this->visit($chapterPage->getUrl())
->see($chapterPage->name);
}
public function test_chapter_create_restriction()
{
$chapter = \BookStack\Chapter::first();
$chapterUrl = $chapter->getUrl();
$this->actingAs($this->user)
->visit($chapterUrl)
->seeInElement('.action-buttons', 'New Page');
$this->setEntityRestrictions($chapter, ['view', 'delete', 'update']);
$this->forceVisit($chapterUrl . '/create-page')
->see('You do not have permission')->seePageIs('/');
$this->visit($chapterUrl)->dontSeeInElement('.action-buttons', 'New Page');
$this->setEntityRestrictions($chapter, ['view', 'create']);
$this->visit($chapterUrl . '/create-page')
->type('test page', 'name')
->type('test content', 'html')
->press('Save Page')
->seePageIs($chapter->book->getUrl() . '/page/test-page');
$this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page');
}
public function test_chapter_update_restriction()
{
$chapter = \BookStack\Chapter::first();
$chapterPage = $chapter->pages->first();
$chapterUrl = $chapter->getUrl();
$this->actingAs($this->user)
->visit($chapterUrl . '/edit')
->see('Edit Chapter');
$this->setEntityRestrictions($chapter, ['view', 'delete']);
$this->forceVisit($chapterUrl . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($chapterPage->getUrl() . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($chapter, ['view', 'update']);
$this->visit($chapterUrl . '/edit')
->seePageIs($chapterUrl . '/edit')->see('Edit Chapter');
$this->visit($chapterPage->getUrl() . '/edit')
->seePageIs($chapterPage->getUrl() . '/edit');
}
public function test_chapter_delete_restriction()
{
$chapter = \BookStack\Chapter::first();
$chapterPage = $chapter->pages->first();
$chapterUrl = $chapter->getUrl();
$this->actingAs($this->user)
->visit($chapterUrl . '/delete')
->see('Delete Chapter');
$this->setEntityRestrictions($chapter, ['view', 'update']);
$this->forceVisit($chapterUrl . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($chapterPage->getUrl() . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($chapter, ['view', 'delete']);
$this->visit($chapterUrl . '/delete')
->seePageIs($chapterUrl . '/delete')->see('Delete Chapter');
$this->visit($chapterPage->getUrl() . '/delete')
->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page');
}
public function test_page_view_restriction()
{
$page = \BookStack\Page::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
->visit($pageUrl)
->seePageIs($pageUrl);
$this->setEntityRestrictions($page, ['update', 'delete']);
$this->forceVisit($pageUrl)
->see('Page not found');
$this->setEntityRestrictions($page, ['view']);
$this->visit($pageUrl)
->see($page->name);
}
public function test_page_update_restriction()
{
$page = \BookStack\Chapter::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
->visit($pageUrl . '/edit')
->seeInField('name', $page->name);
$this->setEntityRestrictions($page, ['view', 'delete']);
$this->forceVisit($pageUrl . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($page, ['view', 'update']);
$this->visit($pageUrl . '/edit')
->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name);
}
public function test_page_delete_restriction()
{
$page = \BookStack\Page::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
->visit($pageUrl . '/delete')
->see('Delete Page');
$this->setEntityRestrictions($page, ['view', 'update']);
$this->forceVisit($pageUrl . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($page, ['view', 'delete']);
$this->visit($pageUrl . '/delete')
->seePageIs($pageUrl . '/delete')->see('Delete Page');
}
public function test_book_restriction_form()
{
$book = \BookStack\Book::first();
$this->asAdmin()->visit($book->getUrl() . '/restrict')
->see('Book Restrictions')
->check('restricted')
->check('restrictions[2][view]')
->press('Save Restrictions')
->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
->seeInDatabase('restrictions', [
'restrictable_id' => $book->id,
'restrictable_type' => 'BookStack\Book',
'role_id' => '2',
'action' => 'view'
]);
}
public function test_chapter_restriction_form()
{
$chapter = \BookStack\Chapter::first();
$this->asAdmin()->visit($chapter->getUrl() . '/restrict')
->see('Chapter Restrictions')
->check('restricted')
->check('restrictions[2][update]')
->press('Save Restrictions')
->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
->seeInDatabase('restrictions', [
'restrictable_id' => $chapter->id,
'restrictable_type' => 'BookStack\Chapter',
'role_id' => '2',
'action' => 'update'
]);
}
public function test_page_restriction_form()
{
$page = \BookStack\Page::first();
$this->asAdmin()->visit($page->getUrl() . '/restrict')
->see('Page Restrictions')
->check('restricted')
->check('restrictions[2][delete]')
->press('Save Restrictions')
->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
->seeInDatabase('restrictions', [
'restrictable_id' => $page->id,
'restrictable_type' => 'BookStack\Page',
'role_id' => '2',
'action' => 'delete'
]);
}
public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
{
$chapter = \BookStack\Chapter::first();
$page = $chapter->pages->first();
$page2 = $chapter->pages[2];
$this->setEntityRestrictions($page, []);
$this->actingAs($this->user)
->visit($page2->getUrl())
->dontSeeInElement('.sidebar-page-list', $page->name);
}
public function test_restricted_pages_not_visible_in_book_navigation_on_chapters()
{
$chapter = \BookStack\Chapter::first();
$page = $chapter->pages->first();
$this->setEntityRestrictions($page, []);
$this->actingAs($this->user)
->visit($chapter->getUrl())
->dontSeeInElement('.sidebar-page-list', $page->name);
}
public function test_restricted_pages_not_visible_on_chapter_pages()
{
$chapter = \BookStack\Chapter::first();
$page = $chapter->pages->first();
$this->setEntityRestrictions($page, []);
$this->actingAs($this->user)
->visit($chapter->getUrl())
->dontSee($page->name);
}
}

View File

@ -1,6 +1,7 @@
<?php
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\DomCrawler\Crawler;
class TestCase extends Illuminate\Foundation\Testing\TestCase
{
@ -122,6 +123,40 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $this;
}
/**
* Assert that the current page matches a given URI.
*
* @param string $uri
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->assertEquals(
$uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
);
return $this;
}
/**
* Do a forced visit that does not error out on exception.
* @param string $uri
* @param array $parameters
* @param array $cookies
* @param array $files
* @return $this
*/
protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
{
$method = 'GET';
$uri = $this->prepareUrlForRequest($uri);
$this->call($method, $uri, $parameters, $cookies, $files);
$this->clearInputs()->followRedirects();
$this->currentUri = $this->app->make('request')->fullUrl();
$this->crawler = new Crawler($this->response->getContent(), $uri);
return $this;
}
/**
* Click the text within the selected element.
* @param $parentElement