]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #1197 from moucho/master
authorDan Brown <redacted>
Sun, 6 Jan 2019 14:37:11 +0000 (14:37 +0000)
committerGitHub <redacted>
Sun, 6 Jan 2019 14:37:11 +0000 (14:37 +0000)
Spanish update

17 files changed:
app/Auth/UserRepo.php
app/Entities/Repos/EntityRepo.php
app/Entities/Repos/PageRepo.php
app/Exceptions/NotifyException.php
app/Exceptions/UserUpdateException.php [new file with mode: 0644]
app/Http/Controllers/ChapterController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/UserController.php
resources/lang/en/errors.php
resources/views/base.blade.php
resources/views/chapters/show.blade.php
resources/views/pages/show.blade.php
resources/views/users/profile.blade.php
tests/Entity/PageContentTest.php
tests/Entity/SortTest.php
tests/Permissions/RolesTest.php
tests/PublicActionTest.php

index d436ab8eb0e72f2eeee6f5f04198ae6a57f2a4a0..31b91108d2b11e1e28dbb233f4f249dc3092ba38 100644 (file)
@@ -3,6 +3,7 @@
 use Activity;
 use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\Image;
 use Exception;
 use Images;
@@ -42,7 +43,7 @@ class UserRepo
      */
     public function getById($id)
     {
-        return $this->user->findOrFail($id);
+        return $this->user->newQuery()->findOrFail($id);
     }
 
     /**
@@ -135,6 +136,40 @@ class UserRepo
         return true;
     }
 
+    /**
+     * Set the assigned user roles via an array of role IDs.
+     * @param User $user
+     * @param array $roles
+     * @throws UserUpdateException
+     */
+    public function setUserRoles(User $user, array $roles)
+    {
+        if ($this->demotingLastAdmin($user, $roles)) {
+            throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
+        }
+
+        $user->roles()->sync($roles);
+    }
+
+    /**
+     * Check if the given user is the last admin and their new roles no longer
+     * contains the admin role.
+     * @param User $user
+     * @param array $newRoles
+     * @return bool
+     */
+    protected function demotingLastAdmin(User $user, array $newRoles) : bool
+    {
+        if ($this->isOnlyAdmin($user)) {
+            $adminRole = $this->role->getSystemRole('admin');
+            if (!in_array(strval($adminRole->id), $newRoles)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Create a new basic instance of user.
      * @param array $data
@@ -143,7 +178,6 @@ class UserRepo
      */
     public function create(array $data, $verifyEmail = false)
     {
-
         return $this->user->forceCreate([
             'name'     => $data['name'],
             'email'    => $data['email'],
index 44fb9ad123dba1ee4de72e70a8bd2e2f90126844..576ed70f00bdedb45597da098cf85e5c0c15a902 100644 (file)
@@ -599,24 +599,48 @@ class EntityRepo
     }
 
     /**
-     * Render the page for viewing, Parsing and performing features such as page transclusion.
+     * Render the page for viewing
      * @param Page $page
-     * @param bool $ignorePermissions
-     * @return mixed|string
+     * @param bool $blankIncludes
+     * @return string
      */
-    public function renderPage(Page $page, $ignorePermissions = false)
+    public function renderPage(Page $page, bool $blankIncludes = false) : string
     {
         $content = $page->html;
+
         if (!config('app.allow_content_scripts')) {
             $content = $this->escapeScripts($content);
         }
 
-        $matches = [];
-        preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
-        if (count($matches[0]) === 0) {
-            return $content;
+        if ($blankIncludes) {
+            $content = $this->blankPageIncludes($content);
+        } else {
+            $content = $this->parsePageIncludes($content);
         }
 
+        return $content;
+    }
+
+    /**
+     * Remove any page include tags within the given HTML.
+     * @param string $html
+     * @return string
+     */
+    protected function blankPageIncludes(string $html) : string
+    {
+        return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
+    }
+
+    /**
+     * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
+     * @param string $html
+     * @return mixed|string
+     */
+    protected function parsePageIncludes(string $html) : string
+    {
+        $matches = [];
+        preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
+
         $topLevelTags = ['table', 'ul', 'ol'];
         foreach ($matches[1] as $index => $includeId) {
             $splitInclude = explode('#', $includeId, 2);
@@ -625,14 +649,14 @@ class EntityRepo
                 continue;
             }
 
-            $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
+            $matchedPage = $this->getById('page', $pageId);
             if ($matchedPage === null) {
-                $content = str_replace($matches[0][$index], '', $content);
+                $html = str_replace($matches[0][$index], '', $html);
                 continue;
             }
 
             if (count($splitInclude) === 1) {
-                $content = str_replace($matches[0][$index], $matchedPage->html, $content);
+                $html = str_replace($matches[0][$index], $matchedPage->html, $html);
                 continue;
             }
 
@@ -640,7 +664,7 @@ class EntityRepo
             $doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
             $matchingElem = $doc->getElementById($splitInclude[1]);
             if ($matchingElem === null) {
-                $content = str_replace($matches[0][$index], '', $content);
+                $html = str_replace($matches[0][$index], '', $html);
                 continue;
             }
             $innerContent = '';
@@ -652,25 +676,22 @@ class EntityRepo
                     $innerContent .= $doc->saveHTML($childNode);
                 }
             }
-            $content = str_replace($matches[0][$index], trim($innerContent), $content);
+            $html = str_replace($matches[0][$index], trim($innerContent), $html);
         }
 
-        return $content;
+        return $html;
     }
 
     /**
      * Escape script tags within HTML content.
      * @param string $html
-     * @return mixed
+     * @return string
      */
-    protected function escapeScripts(string $html)
+    protected function escapeScripts(string $html) : string
     {
         $scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
         $matches = [];
         preg_match_all($scriptSearchRegex, $html, $matches);
-        if (count($matches) === 0) {
-            return $html;
-        }
 
         foreach ($matches[0] as $match) {
             $html = str_replace($match, htmlentities($match), $html);
index d9f9c27206ad8e3c726dbb72700781edb5cafef3..3558b29b3d2e109d6b901ee7adc641856c8876fc 100644 (file)
@@ -194,9 +194,9 @@ class PageRepo extends EntityRepo
      * @param \BookStack\Entities\Page $page
      * @return string
      */
-    public function pageToPlainText(Page $page)
+    protected function pageToPlainText(Page $page) : string
     {
-        $html = $this->renderPage($page);
+        $html = $this->renderPage($page, true);
         return strip_tags($html);
     }
 
index df96b5d12d204c660fc06b36bbe7cd2bf7baaeaf..78ffde05c07e28655ad2cb25ca5a4f04553d2dfc 100644 (file)
@@ -11,7 +11,7 @@ class NotifyException extends \Exception
      * @param string $message
      * @param string    $redirectLocation
      */
-    public function __construct($message, $redirectLocation)
+    public function __construct(string $message, string $redirectLocation = "/")
     {
         $this->message = $message;
         $this->redirectLocation = $redirectLocation;
diff --git a/app/Exceptions/UserUpdateException.php b/app/Exceptions/UserUpdateException.php
new file mode 100644 (file)
index 0000000..eb41dec
--- /dev/null
@@ -0,0 +1,3 @@
+<?php namespace BookStack\Exceptions;
+
+class UserUpdateException extends NotifyException {}
index a50306552443b235d60424c8c818fccaecb0ff4b..20ab9613318947d962997953d865413f882134cd 100644 (file)
@@ -161,6 +161,7 @@ class ChapterController extends Controller
         $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
         $this->checkOwnablePermission('chapter-update', $chapter);
+        $this->checkOwnablePermission('chapter-delete', $chapter);
         return view('chapters/move', [
             'chapter' => $chapter,
             'book' => $chapter->book
@@ -179,6 +180,7 @@ class ChapterController extends Controller
     {
         $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
         $this->checkOwnablePermission('chapter-update', $chapter);
+        $this->checkOwnablePermission('chapter-delete', $chapter);
 
         $entitySelection = $request->get('entity_selection', null);
         if ($entitySelection === null || $entitySelection === '') {
index 74595443b130c5137ec5608e89591749e794e2a6..b68655241485c572a0ea277f400ffd98e880f461 100644 (file)
@@ -586,6 +586,7 @@ class PageController extends Controller
     {
         $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
+        $this->checkOwnablePermission('page-delete', $page);
         return view('pages/move', [
             'book' => $page->book,
             'page' => $page
@@ -604,6 +605,7 @@ class PageController extends Controller
     {
         $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
         $this->checkOwnablePermission('page-update', $page);
+        $this->checkOwnablePermission('page-delete', $page);
 
         $entitySelection = $request->get('entity_selection', null);
         if ($entitySelection === null || $entitySelection === '') {
index 24f8b67cbc4663a83e6777d585326b7759bc9759..cc5ada3f283d2bd4e24f01cc9235e637b0ffa9a1 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\Auth\Access\SocialAuthService;
 use BookStack\Auth\User;
 use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\UserUpdateException;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 
@@ -15,7 +16,7 @@ class UserController extends Controller
     /**
      * UserController constructor.
      * @param User     $user
-     * @param \BookStack\Auth\UserRepo $userRepo
+     * @param UserRepo $userRepo
      */
     public function __construct(User $user, UserRepo $userRepo)
     {
@@ -59,6 +60,7 @@ class UserController extends Controller
      * Store a newly created user in storage.
      * @param  Request $request
      * @return Response
+     * @throws UserUpdateException
      */
     public function store(Request $request)
     {
@@ -89,7 +91,7 @@ class UserController extends Controller
 
         if ($request->filled('roles')) {
             $roles = $request->get('roles');
-            $user->roles()->sync($roles);
+            $this->userRepo->setUserRoles($user, $roles);
         }
 
         $this->userRepo->downloadAndAssignUserAvatar($user);
@@ -122,8 +124,9 @@ class UserController extends Controller
     /**
      * Update the specified user in storage.
      * @param  Request $request
-     * @param  int     $id
+     * @param  int $id
      * @return Response
+     * @throws UserUpdateException
      */
     public function update(Request $request, $id)
     {
@@ -140,13 +143,13 @@ class UserController extends Controller
             'setting'          => 'array'
         ]);
 
-        $user = $this->user->findOrFail($id);
+        $user = $this->userRepo->getById($id);
         $user->fill($request->all());
 
         // Role updates
         if (userCan('users-manage') && $request->filled('roles')) {
             $roles = $request->get('roles');
-            $user->roles()->sync($roles);
+            $this->userRepo->setUserRoles($user, $roles);
         }
 
         // Password updates
@@ -185,7 +188,7 @@ class UserController extends Controller
             return $this->currentUser->id == $id;
         });
 
-        $user = $this->user->findOrFail($id);
+        $user = $this->userRepo->getById($id);
         $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
         return view('users/delete', ['user' => $user]);
     }
@@ -194,6 +197,7 @@ class UserController extends Controller
      * Remove the specified user from storage.
      * @param  int $id
      * @return Response
+     * @throws \Exception
      */
     public function destroy($id)
     {
@@ -279,7 +283,7 @@ class UserController extends Controller
             $viewType = 'list';
         }
 
-        $user = $this->user->findOrFail($id);
+        $user = $this->userRepo->getById($id);
         setting()->putUser($user, 'bookshelves_view_type', $viewType);
 
         return redirect()->back(302, [], "/settings/users/$id");
index 7a881e021a70ba94db76e380d7f71e8f4612c460..b91a0c3e11c96cfdc6f85d4f9940c472476610b2 100644 (file)
@@ -64,6 +64,7 @@ return [
     'role_cannot_be_edited' => 'This role cannot be edited',
     'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
     'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
+    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',
 
     // Comments
     'comment_list' => 'An error occurred while fetching the comments.',
index e6d0b776101f2773ee254883000ff990f129e796..c7a5acca80a0398fdfe5bb58fedf9c88f7bf4ef5 100644 (file)
                             @if(signedInUser() && userCan('settings-manage'))
                                 <a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
                             @endif
+                            @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
+                                <a href="{{ baseUrl('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
+                            @endif
                             @if(!signedInUser())
+                                @if(setting('registration-enabled', false))
+                                    <a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
+                                @endif
                                 <a href="{{ baseUrl('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
                             @endif
                         </div>
index ae450b8ee2e57465b14d1254aff6973ae3d4996c..f5f9901450fc6aa41f2f4c4ed5558b5e2c242617 100644 (file)
             @if(userCan('chapter-update', $chapter))
                 <a href="{{ $chapter->getUrl('/edit') }}" class="text-primary text-button">@icon('edit'){{ trans('common.edit') }}</a>
             @endif
-            @if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
+            @if((userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter) )|| userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
                 <div dropdown class="dropdown-container">
                     <a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
                     <ul>
-                        @if(userCan('chapter-update', $chapter))
+                        @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
                             <li><a href="{{ $chapter->getUrl('/move') }}" class="text-primary">@icon('folder'){{ trans('common.move') }}</a></li>
                         @endif
                         @if(userCan('restrictions-manage', $chapter))
index 0b6aa7d14bbba3c4e395486d3a6ac8d2e69ace53..afe007d45fe057df7dc24f58065d40a1985b41a0 100644 (file)
@@ -23,7 +23,9 @@
                     <ul>
                         @if(userCan('page-update', $page))
                             <li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
-                            <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
+                            @if(userCan('page-delete', $page))
+                                <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
+                            @endif
                             <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
                         @endif
                         @if(userCan('restrictions-manage', $page))
index bd63ce93822773cdcf7fa307d38d0c6048a63ab9..4f67f1be2a59ba90b880988425a942cb4d21b7ee 100644 (file)
             </div>
             <div class="col-md-5 text-bigger" id="content-counts">
                 <div class="text-muted">{{ trans('entities.profile_created_content') }}</div>
-                <div class="text-book">
-                    @icon('book')  {{ trans_choice('entities.x_books', $assetCounts['books']) }}
-                </div>
-                <div class="text-chapter">
-                    @icon('chapter') {{ trans_choice('entities.x_chapters', $assetCounts['chapters']) }}
-                </div>
-                <div class="text-page">
-                    @icon('page') {{ trans_choice('entities.x_pages', $assetCounts['pages']) }}
-                </div>
+                <a href="#recent-books">
+                    <div class="text-book">
+                        @icon('book')  {{ trans_choice('entities.x_books', $assetCounts['books']) }}
+                    </div>
+                </a>
+                <a href="#recent-chapters">
+                    <div class="text-chapter">
+                        @icon('chapter') {{ trans_choice('entities.x_chapters', $assetCounts['chapters']) }}
+                    </div>
+                </a>
+                <a href="#recent-pages">
+                    <div class="text-page">
+                        @icon('page') {{ trans_choice('entities.x_pages', $assetCounts['pages']) }}
+                    </div>
+                </a>
             </div>
         </div>
 
 
         <hr class="even">
-
-        <h3>{{ trans('entities.recently_created_pages') }}</h3>
+        <h3 id="recent-pages">{{ trans('entities.recently_created_pages') }}</h3>
         @if (count($recentlyCreated['pages']) > 0)
             @include('partials/entity-list', ['entities' => $recentlyCreated['pages']])
         @else
@@ -60,8 +65,7 @@
         @endif
 
         <hr class="even">
-
-        <h3>{{ trans('entities.recently_created_chapters') }}</h3>
+        <h3 id="recent-chapters">{{ trans('entities.recently_created_chapters') }}</h3>
         @if (count($recentlyCreated['chapters']) > 0)
             @include('partials/entity-list', ['entities' => $recentlyCreated['chapters']])
         @else
@@ -69,8 +73,7 @@
         @endif
 
         <hr class="even">
-
-        <h3>{{ trans('entities.recently_created_books') }}</h3>
+        <h3 id="recent-books">{{ trans('entities.recently_created_books') }}</h3>
         @if (count($recentlyCreated['books']) > 0)
             @include('partials/entity-list', ['entities' => $recentlyCreated['books']])
         @else
index 6a7112bcbac45135353ded3b7e2e4293efff4f70..86abadf147a788d63684a8a71a3a616ef3d57766 100644 (file)
@@ -40,15 +40,18 @@ class PageContentTest extends TestCase
     {
         $page = Page::first();
         $secondPage = Page::where('id', '!=', $page->id)->first();
+
         $this->asEditor();
-        $page->html = "<p>{{@$secondPage->id}}</p>";
+        $includeTag = '{{@' . $secondPage->id . '}}';
+        $page->html = '<p>' . $includeTag . '</p>';
 
         $resp = $this->put($page->getUrl(), ['name' => $page->name, 'html' => $page->html, 'summary' => '']);
 
         $resp->assertStatus(302);
 
         $page = Page::find($page->id);
-        $this->assertContains("{{@$secondPage->id}}", $page->html);
+        $this->assertContains($includeTag, $page->html);
+        $this->assertEquals('', $page->text);
     }
 
     public function test_page_includes_do_not_break_tables()
index 5b23acfd58461adb295af41f71d84272d3397e97..11294f7dfdcd236dc7b0f6f8508650a09656a5e6 100644 (file)
@@ -3,7 +3,6 @@
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
 use BookStack\Entities\Repos\PageRepo;
 
 class SortTest extends TestCase
@@ -58,14 +57,14 @@ class SortTest extends TestCase
         $newBook = Book::where('id', '!=', $currentBook->id)->first();
         $editor = $this->getEditor();
 
-        $this->setEntityRestrictions($newBook, ['view', 'edit', 'delete'], $editor->roles);
+        $this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], $editor->roles);
 
         $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
             'entity_selection' => 'book:' . $newBook->id
         ]);
         $this->assertPermissionError($movePageResp);
 
-        $this->setEntityRestrictions($newBook, ['view', 'edit', 'delete', 'create'], $editor->roles);
+        $this->setEntityRestrictions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles);
         $movePageResp = $this->put($page->getUrl('/move'), [
             'entity_selection' => 'book:' . $newBook->id
         ]);
@@ -76,6 +75,33 @@ class SortTest extends TestCase
         $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
     }
 
+    public function test_page_move_requires_delete_permissions()
+    {
+        $page = Page::first();
+        $currentBook = $page->book;
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
+        $editor = $this->getEditor();
+
+        $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles);
+        $this->setEntityRestrictions($page, ['view', 'update', 'create'], $editor->roles);
+
+        $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
+            'entity_selection' => 'book:' . $newBook->id
+        ]);
+        $this->assertPermissionError($movePageResp);
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertDontSee($page->getUrl('/move'));
+
+        $this->setEntityRestrictions($page, ['view', 'update', 'create', 'delete'], $editor->roles);
+        $movePageResp = $this->put($page->getUrl('/move'), [
+            'entity_selection' => 'book:' . $newBook->id
+        ]);
+
+        $page = Page::find($page->id);
+        $movePageResp->assertRedirect($page->getUrl());
+        $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
+    }
+
     public function test_chapter_move()
     {
         $chapter = Chapter::first();
@@ -104,6 +130,33 @@ class SortTest extends TestCase
         $pageCheckResp->assertSee($newBook->name);
     }
 
+    public function test_chapter_move_requires_delete_permissions()
+    {
+        $chapter = Chapter::first();
+        $currentBook = $chapter->book;
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
+        $editor = $this->getEditor();
+
+        $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles);
+        $this->setEntityRestrictions($chapter, ['view', 'update', 'create'], $editor->roles);
+
+        $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [
+            'entity_selection' => 'book:' . $newBook->id
+        ]);
+        $this->assertPermissionError($moveChapterResp);
+        $pageView = $this->get($chapter->getUrl());
+        $pageView->assertDontSee($chapter->getUrl('/move'));
+
+        $this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles);
+        $moveChapterResp = $this->put($chapter->getUrl('/move'), [
+            'entity_selection' => 'book:' . $newBook->id
+        ]);
+
+        $chapter = Chapter::find($chapter->id);
+        $moveChapterResp->assertRedirect($chapter->getUrl());
+        $this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');
+    }
+
     public function test_book_sort()
     {
         $oldBook = Book::query()->first();
index 95cb7cd5726ec7e54bd31902a86511f6a745386b..da2abb0bdf23c335fe5aa29a1053ce71deac8007 100644 (file)
@@ -78,6 +78,28 @@ class RolesTest extends BrowserKitTest
             ->dontSee($testRoleUpdateName);
     }
 
+    public function test_admin_role_cannot_be_removed_if_last_admin()
+    {
+        $adminRole = Role::where('system_name', '=', 'admin')->first();
+        $adminUser = $this->getAdmin();
+        $adminRole->users()->where('id', '!=', $adminUser->id)->delete();
+        $this->assertEquals($adminRole->users()->count(), 1);
+
+        $viewerRole = $this->getViewer()->roles()->first();
+
+        $editUrl = '/settings/users/' . $adminUser->id;
+        $this->actingAs($adminUser)->put($editUrl, [
+            'name' => $adminUser->name,
+            'email' => $adminUser->email,
+            'roles' => [
+                'viewer' => strval($viewerRole->id),
+            ]
+        ])->followRedirects();
+
+        $this->seePageIs($editUrl);
+        $this->see('This user is the only user assigned to the administrator role');
+    }
+
     public function test_manage_user_permission()
     {
         $this->actingAs($this->user)->visit('/settings/users')
@@ -87,6 +109,16 @@ class RolesTest extends BrowserKitTest
             ->seePageIs('/settings/users');
     }
 
+    public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision()
+    {
+        $usersLink = 'href="'.url('/settings/users') . '"';
+        $this->actingAs($this->user)->visit('/')->dontSee($usersLink);
+        $this->giveUserPermissions($this->user, ['users-manage']);
+        $this->actingAs($this->user)->visit('/')->see($usersLink);
+        $this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']);
+        $this->actingAs($this->user)->visit('/')->dontSee($usersLink);
+    }
+
     public function test_user_roles_manage_permission()
     {
         $this->actingAs($this->user)->visit('/settings/roles')
index 671b5ee420453d266618b09a2f795de1c124d7dc..27b4822fa2bce01f5379b50e3558333c01846003 100644 (file)
@@ -14,6 +14,24 @@ class PublicActionTest extends BrowserKitTest
         $this->visit($page->getUrl())->seePageIs('/login');
     }
 
+    public function test_login_link_visible()
+    {
+        $this->setSettings(['app-public' => 'true']);
+        $this->visit('/')->see(url('/login'));
+    }
+
+    public function test_register_link_visible_when_enabled()
+    {
+        $this->setSettings(['app-public' => 'true']);
+
+        $this->visit('/')->see(url('/login'));
+        $this->visit('/')->dontSee(url('/register'));
+
+        $this->setSettings(['app-public' => 'true', 'registration-enabled' => 'true']);
+        $this->visit('/')->see(url('/login'));
+        $this->visit('/')->see(url('/register'));
+    }
+
     public function test_books_viewable()
     {
         $this->setSettings(['app-public' => 'true']);