mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-10-22 07:52:19 +03:00
As per PR #5800 * DB: Planned out new entity table format via migrations * DB: Created entity migration logic Made some other tweaks/fixes while testing. * DB: Added change of entity relation columns to suit new entities table * DB: Got most view queries working for new structure * Entities: Started logic change to new structure Updated base entity class, and worked through BaseRepo. Need to go through other repos next. Removed a couple of redundant interfaces as part of this since we can move the logic onto the shared ContainerData model as needed. * Entities: Been through repos to update for new format * Entities: Updated repos to act on refreshed clones Changes to core entity models are now done on clones to ensure clean state before save, and those clones are returned back if changes are needed after that action. * Entities: Updated model classes & relations for changes * Entities: Changed from *Data to a common "contents" system Added smart loading from builder instances which should hydrate with "contents()" loaded via join, while keeping the core model original. * Entities: Moved entity description/covers to own non-model classes Added back some interfaces. * Entities: Removed use of contents system for data access * Entities: Got most queries back to working order * Entities: Reverted back to data from contents, fixed various issues * Entities: Started addressing issues from tests * Entities: Addressed further tests/issues * Entities: Been through tests to get all passing in dev Fixed issues and needed test changes along the way. * Entities: Addressed phpstan errors * Entities: Reviewed TODO notes * Entities: Ensured book/shelf relation data removed on destroy * Entities: Been through API responses & adjusted field visibility * Entities: Added type index to massively improve query speed
396 lines
15 KiB
PHP
396 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Tests\Entity;
|
|
|
|
use BookStack\Entities\Models\Book;
|
|
use BookStack\Entities\Models\BookChild;
|
|
use BookStack\Entities\Models\Bookshelf;
|
|
use BookStack\Entities\Repos\BookRepo;
|
|
use Tests\TestCase;
|
|
|
|
class BookTest extends TestCase
|
|
{
|
|
public function test_create()
|
|
{
|
|
$book = Book::factory()->make([
|
|
'name' => 'My First Book',
|
|
]);
|
|
|
|
$resp = $this->asEditor()->get('/books');
|
|
$this->withHtml($resp)->assertElementContains('a[href="' . url('/create-book') . '"]', 'Create New Book');
|
|
|
|
$resp = $this->get('/create-book');
|
|
$this->withHtml($resp)->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book');
|
|
|
|
$resp = $this->post('/books', $book->only('name', 'description_html'));
|
|
$resp->assertRedirect('/books/my-first-book');
|
|
|
|
$resp = $this->get('/books/my-first-book');
|
|
$resp->assertSee($book->name);
|
|
$resp->assertSee($book->descriptionInfo()->getPlain());
|
|
}
|
|
|
|
public function test_create_uses_different_slugs_when_name_reused()
|
|
{
|
|
$book = Book::factory()->make([
|
|
'name' => 'My First Book',
|
|
]);
|
|
|
|
$this->asEditor()->post('/books', $book->only('name', 'description_html'));
|
|
$this->asEditor()->post('/books', $book->only('name', 'description_html'));
|
|
|
|
$books = Book::query()->where('name', '=', $book->name)
|
|
->orderBy('id', 'desc')
|
|
->take(2)
|
|
->get();
|
|
|
|
$this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug);
|
|
$this->assertEquals('my-first-book', $books[1]->slug);
|
|
}
|
|
|
|
public function test_create_sets_tags()
|
|
{
|
|
// Cheeky initial update to refresh slug
|
|
$this->asEditor()->post('books', [
|
|
'name' => 'My book with tags',
|
|
'description_html' => '<p>A book with tags</p>',
|
|
'tags' => [
|
|
[
|
|
'name' => 'Category',
|
|
'value' => 'Donkey Content',
|
|
],
|
|
[
|
|
'name' => 'Level',
|
|
'value' => '5',
|
|
],
|
|
],
|
|
]);
|
|
|
|
/** @var Book $book */
|
|
$book = Book::query()->where('name', '=', 'My book with tags')->firstOrFail();
|
|
$tags = $book->tags()->get();
|
|
|
|
$this->assertEquals(2, $tags->count());
|
|
$this->assertEquals('Donkey Content', $tags[0]->value);
|
|
$this->assertEquals('Level', $tags[1]->name);
|
|
}
|
|
|
|
public function test_update()
|
|
{
|
|
$book = $this->entities->book();
|
|
// Cheeky initial update to refresh slug
|
|
$this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description_html' => $book->description_html]);
|
|
$book->refresh();
|
|
|
|
$newName = $book->name . ' Updated';
|
|
$newDesc = $book->description_html . '<p>with more content</p>';
|
|
|
|
$resp = $this->get($book->getUrl('/edit'));
|
|
$resp->assertSee($book->name);
|
|
$resp->assertSee($book->description_html);
|
|
$this->withHtml($resp)->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book');
|
|
|
|
$resp = $this->put($book->getUrl(), ['name' => $newName, 'description_html' => $newDesc]);
|
|
$resp->assertRedirect($book->getUrl() . '-updated');
|
|
|
|
$resp = $this->get($book->getUrl() . '-updated');
|
|
$resp->assertSee($newName);
|
|
$resp->assertSee($newDesc, false);
|
|
}
|
|
|
|
public function test_update_sets_tags()
|
|
{
|
|
$book = $this->entities->book();
|
|
|
|
$this->assertEquals(0, $book->tags()->count());
|
|
|
|
// Cheeky initial update to refresh slug
|
|
$this->asEditor()->put($book->getUrl(), [
|
|
'name' => $book->name,
|
|
'tags' => [
|
|
[
|
|
'name' => 'Category',
|
|
'value' => 'Dolphin Content',
|
|
],
|
|
[
|
|
'name' => 'Level',
|
|
'value' => '5',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$book->refresh();
|
|
$tags = $book->tags()->get();
|
|
|
|
$this->assertEquals(2, $tags->count());
|
|
$this->assertEquals('Dolphin Content', $tags[0]->value);
|
|
$this->assertEquals('Level', $tags[1]->name);
|
|
}
|
|
|
|
public function test_delete()
|
|
{
|
|
$book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
|
|
$this->assertNull($book->deleted_at);
|
|
$pageCount = $book->pages()->count();
|
|
$chapterCount = $book->chapters()->count();
|
|
|
|
$deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
|
|
$deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
|
|
|
|
$deleteReq = $this->delete($book->getUrl());
|
|
$deleteReq->assertRedirect(url('/books'));
|
|
$this->assertActivityExists('book_delete', $book);
|
|
|
|
$book->refresh();
|
|
$this->assertNotNull($book->deleted_at);
|
|
|
|
$this->assertTrue($book->pages()->count() === 0);
|
|
$this->assertTrue($book->chapters()->count() === 0);
|
|
$this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
|
|
$this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
|
|
$this->assertTrue($book->deletions()->count() === 1);
|
|
|
|
$redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
|
|
$this->assertNotificationContains($redirectReq, 'Book Successfully Deleted');
|
|
}
|
|
|
|
public function test_cancel_on_create_page_leads_back_to_books_listing()
|
|
{
|
|
$resp = $this->asEditor()->get('/create-book');
|
|
$this->withHtml($resp)->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel');
|
|
}
|
|
|
|
public function test_cancel_on_edit_book_page_leads_back_to_book()
|
|
{
|
|
$book = $this->entities->book();
|
|
$resp = $this->asEditor()->get($book->getUrl('/edit'));
|
|
$this->withHtml($resp)->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel');
|
|
}
|
|
|
|
public function test_next_previous_navigation_controls_show_within_book_content()
|
|
{
|
|
$book = $this->entities->book();
|
|
$chapter = $book->chapters->first();
|
|
|
|
$resp = $this->asEditor()->get($chapter->getUrl());
|
|
$this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Next');
|
|
$this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[0]->name, 0, 20));
|
|
|
|
$resp = $this->get($chapter->pages[0]->getUrl());
|
|
$this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[1]->name, 0, 20));
|
|
$this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Previous');
|
|
$this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));
|
|
}
|
|
|
|
public function test_recently_viewed_books_updates_as_expected()
|
|
{
|
|
$books = Book::take(2)->get();
|
|
|
|
$resp = $this->asAdmin()->get('/books');
|
|
$this->withHtml($resp)->assertElementNotContains('#recents', $books[0]->name)
|
|
->assertElementNotContains('#recents', $books[1]->name);
|
|
|
|
$this->get($books[0]->getUrl());
|
|
$this->get($books[1]->getUrl());
|
|
|
|
$resp = $this->get('/books');
|
|
$this->withHtml($resp)->assertElementContains('#recents', $books[0]->name)
|
|
->assertElementContains('#recents', $books[1]->name);
|
|
}
|
|
|
|
public function test_popular_books_updates_upon_visits()
|
|
{
|
|
$books = Book::take(2)->get();
|
|
|
|
$resp = $this->asAdmin()->get('/books');
|
|
$this->withHtml($resp)->assertElementNotContains('#popular', $books[0]->name)
|
|
->assertElementNotContains('#popular', $books[1]->name);
|
|
|
|
$this->get($books[0]->getUrl());
|
|
$this->get($books[1]->getUrl());
|
|
$this->get($books[0]->getUrl());
|
|
|
|
$resp = $this->get('/books');
|
|
$this->withHtml($resp)->assertElementContains('#popular .book:nth-child(1)', $books[0]->name)
|
|
->assertElementContains('#popular .book:nth-child(2)', $books[1]->name);
|
|
}
|
|
|
|
public function test_books_view_shows_view_toggle_option()
|
|
{
|
|
/** @var Book $book */
|
|
$editor = $this->users->editor();
|
|
setting()->putUser($editor, 'books_view_type', 'list');
|
|
|
|
$resp = $this->actingAs($editor)->get('/books');
|
|
$this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'Grid View');
|
|
$this->withHtml($resp)->assertElementExists('button[name="view"][value="grid"]');
|
|
|
|
$resp = $this->patch("/preferences/change-view/books", ['view' => 'grid']);
|
|
$resp->assertRedirect();
|
|
$this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
|
|
|
|
$resp = $this->actingAs($editor)->get('/books');
|
|
$this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'List View');
|
|
$this->withHtml($resp)->assertElementExists('button[name="view"][value="list"]');
|
|
|
|
$resp = $this->patch("/preferences/change-view/books", ['view_type' => 'list']);
|
|
$resp->assertRedirect();
|
|
$this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
|
|
}
|
|
|
|
public function test_slug_multi_byte_url_safe()
|
|
{
|
|
$book = $this->entities->newBook([
|
|
'name' => 'информация',
|
|
]);
|
|
|
|
$this->assertEquals('informaciia', $book->slug);
|
|
|
|
$book = $this->entities->newBook([
|
|
'name' => '¿Qué?',
|
|
]);
|
|
|
|
$this->assertEquals('que', $book->slug);
|
|
}
|
|
|
|
public function test_slug_format()
|
|
{
|
|
$book = $this->entities->newBook([
|
|
'name' => 'PartA / PartB / PartC',
|
|
]);
|
|
|
|
$this->assertEquals('parta-partb-partc', $book->slug);
|
|
}
|
|
|
|
public function test_description_limited_to_specific_html()
|
|
{
|
|
$book = $this->entities->book();
|
|
|
|
$input = '<h1>Test</h1><p id="abc" href="beans">Content<a href="#cat" target="_blank" data-a="b">a</a><section>Hello</section></p>';
|
|
$expected = '<p>Content<a href="#cat" target="_blank">a</a></p>';
|
|
|
|
$this->asEditor()->put($book->getUrl(), [
|
|
'name' => $book->name,
|
|
'description_html' => $input
|
|
]);
|
|
|
|
$book->refresh();
|
|
$this->assertEquals($expected, $book->description_html);
|
|
}
|
|
|
|
public function test_show_view_displays_description_if_no_description_html_set()
|
|
{
|
|
$book = $this->entities->book();
|
|
$book->description_html = '';
|
|
$book->description = "My great\ndescription\n\nwith newlines";
|
|
$book->save();
|
|
|
|
$resp = $this->asEditor()->get($book->getUrl());
|
|
$resp->assertSee("<p>My great<br>\ndescription<br>\n<br>\nwith newlines</p>", false);
|
|
}
|
|
|
|
public function test_show_view_has_copy_button()
|
|
{
|
|
$book = $this->entities->book();
|
|
$resp = $this->asEditor()->get($book->getUrl());
|
|
|
|
$this->withHtml($resp)->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy');
|
|
}
|
|
|
|
public function test_copy_view()
|
|
{
|
|
$book = $this->entities->book();
|
|
$resp = $this->asEditor()->get($book->getUrl('/copy'));
|
|
|
|
$resp->assertOk();
|
|
$resp->assertSee('Copy Book');
|
|
$this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]");
|
|
}
|
|
|
|
public function test_copy()
|
|
{
|
|
/** @var Book $book */
|
|
$book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
|
|
$resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
|
|
|
|
/** @var Book $copy */
|
|
$copy = Book::query()->where('name', '=', 'My copy book')->first();
|
|
|
|
$resp->assertRedirect($copy->getUrl());
|
|
$this->assertEquals($book->getDirectVisibleChildren()->count(), $copy->getDirectVisibleChildren()->count());
|
|
|
|
$this->get($copy->getUrl())->assertSee($book->description_html, false);
|
|
}
|
|
|
|
public function test_copy_does_not_copy_non_visible_content()
|
|
{
|
|
/** @var Book $book */
|
|
$book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
|
|
|
|
// Hide child content
|
|
/** @var BookChild $page */
|
|
foreach ($book->getDirectVisibleChildren() as $child) {
|
|
$this->permissions->setEntityPermissions($child, [], []);
|
|
}
|
|
|
|
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
|
|
/** @var Book $copy */
|
|
$copy = Book::query()->where('name', '=', 'My copy book')->first();
|
|
|
|
$this->assertEquals(0, $copy->getDirectVisibleChildren()->count());
|
|
}
|
|
|
|
public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
|
|
{
|
|
/** @var Book $book */
|
|
$book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
|
|
$viewer = $this->users->viewer();
|
|
$this->permissions->grantUserRolePermissions($viewer, ['book-create-all']);
|
|
|
|
$this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
|
|
/** @var Book $copy */
|
|
$copy = Book::query()->where('name', '=', 'My copy book')->first();
|
|
|
|
$this->assertEquals(0, $copy->pages()->count());
|
|
$this->assertEquals(0, $copy->chapters()->count());
|
|
}
|
|
|
|
public function test_copy_clones_cover_image_if_existing()
|
|
{
|
|
$book = $this->entities->book();
|
|
$bookRepo = $this->app->make(BookRepo::class);
|
|
$coverImageFile = $this->files->uploadedImage('cover.png');
|
|
$bookRepo->updateCoverImage($book, $coverImageFile);
|
|
|
|
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book'])->assertRedirect();
|
|
/** @var Book $copy */
|
|
$copy = Book::query()->where('name', '=', 'My copy book')->first();
|
|
|
|
$this->assertNotNull($copy->coverInfo()->getImage());
|
|
$this->assertNotEquals($book->coverInfo()->getImage()->id, $copy->coverInfo()->getImage()->id);
|
|
}
|
|
|
|
public function test_copy_adds_book_to_shelves_if_edit_permissions_allows()
|
|
{
|
|
/** @var Bookshelf $shelfA */
|
|
/** @var Bookshelf $shelfB */
|
|
[$shelfA, $shelfB] = Bookshelf::query()->take(2)->get();
|
|
$book = $this->entities->book();
|
|
|
|
$shelfA->appendBook($book);
|
|
$shelfB->appendBook($book);
|
|
|
|
$viewer = $this->users->viewer();
|
|
$this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']);
|
|
$this->permissions->setEntityPermissions($shelfB);
|
|
|
|
|
|
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
|
|
/** @var Book $copy */
|
|
$copy = Book::query()->where('name', '=', 'My copy book')->first();
|
|
|
|
$this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists());
|
|
$this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists());
|
|
}
|
|
}
|