]> BookStack Code Mirror - bookstack/commitdiff
Added chapters to the API
authorDan Brown <redacted>
Fri, 22 May 2020 23:28:41 +0000 (00:28 +0100)
committerDan Brown <redacted>
Fri, 22 May 2020 23:28:41 +0000 (00:28 +0100)
24 files changed:
app/Actions/Tag.php
app/Actions/TagRepo.php
app/Auth/User.php
app/Entities/Book.php
app/Entities/Bookshelf.php
app/Entities/Chapter.php
app/Entities/Entity.php
app/Entities/Page.php
app/Entities/Repos/PageRepo.php
app/Http/Controllers/Api/BookApiController.php [moved from app/Http/Controllers/Api/BooksApiController.php with 96% similarity]
app/Http/Controllers/Api/BookExportApiController.php [moved from app/Http/Controllers/Api/BooksExportApiController.php with 96% similarity]
app/Http/Controllers/Api/ChapterApiController.php [new file with mode: 0644]
app/Http/Controllers/Api/ChapterExportApiController.php [new file with mode: 0644]
dev/api/requests/chapters-create.json [new file with mode: 0644]
dev/api/requests/chapters-update.json [new file with mode: 0644]
dev/api/responses/books-read.json
dev/api/responses/chapters-create.json [new file with mode: 0644]
dev/api/responses/chapters-list.json [new file with mode: 0644]
dev/api/responses/chapters-read.json [new file with mode: 0644]
dev/api/responses/chapters-update.json [new file with mode: 0644]
dev/api/responses/shelves-read.json
routes/api.php
tests/Api/ChaptersApiTest.php [new file with mode: 0644]
tests/Api/TestsApi.php

index 38d0458e46425f53fd13c6421d745c8fe67b0101..80a91150868e9cd87be62685891237c392606328 100644 (file)
@@ -9,6 +9,7 @@ use BookStack\Model;
 class Tag extends Model
 {
     protected $fillable = ['name', 'value', 'order'];
+    protected $hidden = ['id', 'entity_id', 'entity_type'];
 
     /**
      * Get the entity that this tag belongs to
index 0cbfa4163bf9120226aafea71e678a4f13b920a8..b8b1eb464f72957b7e211cda8555b1a9d7bdc274 100644 (file)
@@ -106,14 +106,13 @@ class TagRepo
 
     /**
      * Save an array of tags to an entity
-     * @param \BookStack\Entities\Entity $entity
-     * @param array $tags
      * @return array|\Illuminate\Database\Eloquent\Collection
      */
-    public function saveTagsToEntity(Entity $entity, $tags = [])
+    public function saveTagsToEntity(Entity $entity, array $tags = [])
     {
         $entity->tags()->delete();
         $newTags = [];
+
         foreach ($tags as $tag) {
             if (trim($tag['name']) === '') {
                 continue;
index a581d999340b1d76294a0c2dbb0a55c4501f6190..40718beb6bfdccc1f7fb53770c7600fffd43b2ed 100644 (file)
@@ -49,7 +49,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     protected $hidden = [
         'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
-        'created_at', 'updated_at',
+        'created_at', 'updated_at', 'image_id',
     ];
 
     /**
index 38b7d4a8c29d3361652192d703a8362029bd62e4..af8344b88f5cb440b9abeb6913e3e55ffeda68f6 100644 (file)
@@ -19,7 +19,7 @@ class Book extends Entity implements HasCoverImage
     public $searchFactor = 2;
 
     protected $fillable = ['name', 'description'];
-    protected $hidden = ['restricted', 'pivot'];
+    protected $hidden = ['restricted', 'pivot', 'image_id'];
 
     /**
      * Get the url for this book.
index c7ba840e0ce0803f5e2da79ddf177bc1bb0dc563..474ba51cd8204bf27dfc95ad421029cdaa8e7375 100644 (file)
@@ -12,7 +12,7 @@ class Bookshelf extends Entity implements HasCoverImage
 
     protected $fillable = ['name', 'description', 'image_id'];
 
-    protected $hidden = ['restricted'];
+    protected $hidden = ['restricted', 'image_id'];
 
     /**
      * Get the books in this shelf.
index 848bc6448bd467b853ef25c8982c0aa8b5ad6e4c..3290afcfa6b90b0b01b653f99dfd7f93598f6556 100644 (file)
@@ -12,6 +12,7 @@ class Chapter extends BookChild
     public $searchFactor = 1.3;
 
     protected $fillable = ['name', 'description', 'priority', 'book_id'];
+    protected $hidden = ['restricted', 'pivot'];
 
     /**
      * Get the pages that this chapter contains.
index 5013c39cfcf2bf309f1931594ebe702dd00dc44c..6a5894cacb91941115e6e01225fd2aa5b100c89b 100644 (file)
@@ -288,7 +288,7 @@ class Entity extends Ownable
     public function rebuildPermissions()
     {
         /** @noinspection PhpUnhandledExceptionInspection */
-        Permissions::buildJointPermissionsForEntity($this);
+        Permissions::buildJointPermissionsForEntity(clone $this);
     }
 
     /**
@@ -297,7 +297,7 @@ class Entity extends Ownable
     public function indexForSearch()
     {
         $searchService = app()->make(SearchService::class);
-        $searchService->indexEntity($this);
+        $searchService->indexEntity(clone $this);
     }
 
     /**
index 76dc628fbf3f59e0bbbcf98897e715b743638213..d10786ddac18177f99197939065d4d3f3033c59f 100644 (file)
@@ -27,6 +27,8 @@ class Page extends BookChild
 
     public $textField = 'text';
 
+    protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
+
     /**
      * Get the entities that are visible to the current user.
      */
index e49eeb1ef5518eb8aa124d11e981236f11ebbe04..d92085a61cf4c786f9c2f0c6210e9363cf8f5611 100644 (file)
@@ -211,7 +211,7 @@ class PageRepo
      */
     protected function savePageRevision(Page $page, string $summary = null)
     {
-        $revision = new PageRevision($page->toArray());
+        $revision = new PageRevision($page->getAttributes());
 
         if (setting('app-editor') !== 'markdown') {
             $revision->markdown = '';
similarity index 96%
rename from app/Http/Controllers/Api/BooksApiController.php
rename to app/Http/Controllers/Api/BookApiController.php
index ac4ea171c7e9633606011f907b2078b52ffe3d86..8333eba3a1d3779431dbbffa39e4e9abd49837d2 100644 (file)
@@ -8,7 +8,7 @@ use Illuminate\Contracts\Container\BindingResolutionException;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 
-class BooksApiController extends ApiController
+class BookApiController extends ApiController
 {
 
     protected $bookRepo;
@@ -17,10 +17,12 @@ class BooksApiController extends ApiController
         'create' => [
             'name' => 'required|string|max:255',
             'description' => 'string|max:1000',
+            'tags' => 'array',
         ],
         'update' => [
             'name' => 'string|min:1|max:255',
             'description' => 'string|max:1000',
+            'tags' => 'array',
         ],
     ];
 
similarity index 96%
rename from app/Http/Controllers/Api/BooksExportApiController.php
rename to app/Http/Controllers/Api/BookExportApiController.php
index 605f8f4089aaa0cd9e40bd47fc27713c329414f9..31fe5250fd7e3f14d7d60b8ff51a5d4bc4c241fd 100644 (file)
@@ -5,9 +5,8 @@ use BookStack\Entities\ExportService;
 use BookStack\Entities\Repos\BookRepo;
 use Throwable;
 
-class BooksExportApiController extends ApiController
+class BookExportApiController extends ApiController
 {
-
     protected $bookRepo;
     protected $exportService;
 
diff --git a/app/Http/Controllers/Api/ChapterApiController.php b/app/Http/Controllers/Api/ChapterApiController.php
new file mode 100644 (file)
index 0000000..50aa883
--- /dev/null
@@ -0,0 +1,104 @@
+<?php namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Facades\Activity;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Http\Request;
+
+class ChapterApiController extends ApiController
+{
+    protected $chapterRepo;
+
+    protected $rules = [
+        'create' => [
+            'book_id' => 'required|integer',
+            'name' => 'required|string|max:255',
+            'description' => 'string|max:1000',
+            'tags' => 'array',
+        ],
+        'update' => [
+            'book_id' => 'integer',
+            'name' => 'string|min:1|max:255',
+            'description' => 'string|max:1000',
+            'tags' => 'array',
+        ],
+    ];
+
+    /**
+     * ChapterController constructor.
+     */
+    public function __construct(ChapterRepo $chapterRepo)
+    {
+        $this->chapterRepo = $chapterRepo;
+    }
+
+    /**
+     * Get a listing of chapters visible to the user.
+     */
+    public function list()
+    {
+        $chapters = Chapter::visible();
+        return $this->apiListingResponse($chapters, [
+            'id', 'book_id', 'name', 'slug', 'description', 'priority',
+            'created_at', 'updated_at', 'created_by', 'updated_by',
+        ]);
+    }
+
+    /**
+     * Create a new chapter in the system.
+     */
+    public function create(Request $request)
+    {
+        $this->validate($request, $this->rules['create']);
+
+        $bookId = $request->get('book_id');
+        $book = Book::visible()->findOrFail($bookId);
+        $this->checkOwnablePermission('chapter-create', $book);
+
+        $chapter = $this->chapterRepo->create($request->all(), $book);
+        Activity::add($chapter, 'chapter_create', $book->id);
+
+        return response()->json($chapter->load(['tags']));
+    }
+
+    /**
+     * View the details of a single chapter.
+     */
+    public function read(string $id)
+    {
+        $chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'pages' => function (HasMany $query) {
+            $query->visible()->get(['id', 'name', 'slug']);
+        }])->findOrFail($id);
+        return response()->json($chapter);
+    }
+
+    /**
+     * Update the details of a single chapter.
+     */
+    public function update(Request $request, string $id)
+    {
+        $chapter = Chapter::visible()->findOrFail($id);
+        $this->checkOwnablePermission('chapter-update', $chapter);
+
+        $updatedChapter = $this->chapterRepo->update($chapter, $request->all());
+        Activity::add($chapter, 'chapter_update', $chapter->book->id);
+
+        return response()->json($updatedChapter->load(['tags']));
+    }
+
+    /**
+     * Delete a chapter from the system.
+     */
+    public function delete(string $id)
+    {
+        $chapter = Chapter::visible()->findOrFail($id);
+        $this->checkOwnablePermission('chapter-delete', $chapter);
+
+        $this->chapterRepo->destroy($chapter);
+        Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
+
+        return response('', 204);
+    }
+}
diff --git a/app/Http/Controllers/Api/ChapterExportApiController.php b/app/Http/Controllers/Api/ChapterExportApiController.php
new file mode 100644 (file)
index 0000000..f19f29e
--- /dev/null
@@ -0,0 +1,54 @@
+<?php namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Chapter;
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\BookRepo;
+use Throwable;
+
+class ChapterExportApiController extends ApiController
+{
+    protected $chapterRepo;
+    protected $exportService;
+
+    /**
+     * ChapterExportController constructor.
+     */
+    public function __construct(BookRepo $chapterRepo, ExportService $exportService)
+    {
+        $this->chapterRepo = $chapterRepo;
+        $this->exportService = $exportService;
+        parent::__construct();
+    }
+
+    /**
+     * Export a chapter as a PDF file.
+     * @throws Throwable
+     */
+    public function exportPdf(int $id)
+    {
+        $chapter = Chapter::visible()->findOrFail($id);
+        $pdfContent = $this->exportService->chapterToPdf($chapter);
+        return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
+    }
+
+    /**
+     * Export a chapter as a contained HTML file.
+     * @throws Throwable
+     */
+    public function exportHtml(int $id)
+    {
+        $chapter = Chapter::visible()->findOrFail($id);
+        $htmlContent = $this->exportService->chapterToContainedHtml($chapter);
+        return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
+    }
+
+    /**
+     * Export a chapter as a plain text file.
+     */
+    public function exportPlainText(int $id)
+    {
+        $chapter = Chapter::visible()->findOrFail($id);
+        $textContent = $this->exportService->chapterToPlainText($chapter);
+        return $this->downloadResponse($textContent, $chapter->slug . '.txt');
+    }
+}
diff --git a/dev/api/requests/chapters-create.json b/dev/api/requests/chapters-create.json
new file mode 100644 (file)
index 0000000..ca06fc2
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "book_id": 1,
+  "name": "My fantastic new chapter",
+  "description": "This is a great new chapter that I've created via the API",
+  "tags": [
+    {"name": "Category", "value": "Top Content"},
+    {"name": "Rating", "value": "Highest"}
+  ]
+}
\ No newline at end of file
diff --git a/dev/api/requests/chapters-update.json b/dev/api/requests/chapters-update.json
new file mode 100644 (file)
index 0000000..6bd3a3e
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "book_id": 1,
+  "name": "My fantastic updated chapter",
+  "description": "This is an updated chapter that I've altered via the API",
+  "tags": [
+    {"name": "Category", "value": "Kinda Good Content"},
+    {"name": "Rating", "value": "Medium"}
+  ]
+}
\ No newline at end of file
index 11408e9ab2df3251081a9cffaf8adf35a16b6c6d..2e43f5f87fc810163bc8323f53e304c6cbb070db 100644 (file)
@@ -7,15 +7,12 @@
   "updated_at": "2020-01-12 14:11:51",
   "created_by": {
     "id": 1,
-    "name": "Admin",
-    "image_id": 48
+    "name": "Admin"
   },
   "updated_by": {
     "id": 1,
-    "name": "Admin",
-    "image_id": 48
+    "name": "Admin"
   },
-  "image_id": 452,
   "tags": [
     {
       "id": 13,
diff --git a/dev/api/responses/chapters-create.json b/dev/api/responses/chapters-create.json
new file mode 100644 (file)
index 0000000..7aac276
--- /dev/null
@@ -0,0 +1,38 @@
+{
+  "book_id": 1,
+  "priority": 6,
+  "name": "My fantastic new chapter",
+  "description": "This is a great new chapter that I've created via the API",
+  "created_by": 1,
+  "updated_by": 1,
+  "slug": "my-fantastic-new-chapter",
+  "updated_at": "2020-05-22 22:59:55",
+  "created_at": "2020-05-22 22:59:55",
+  "id": 74,
+  "book": {
+    "id": 1,
+    "name": "BookStack User Guide",
+    "slug": "bookstack-user-guide",
+    "description": "This is a general guide on using BookStack on a day-to-day basis.",
+    "created_at": "2019-05-05 21:48:46",
+    "updated_at": "2019-12-11 20:57:31",
+    "created_by": 1,
+    "updated_by": 1
+  },
+  "tags": [
+    {
+      "name": "Category",
+      "value": "Top Content",
+      "order": 0,
+      "created_at": "2020-05-22 22:59:55",
+      "updated_at": "2020-05-22 22:59:55"
+    },
+    {
+      "name": "Rating",
+      "value": "Highest",
+      "order": 0,
+      "created_at": "2020-05-22 22:59:55",
+      "updated_at": "2020-05-22 22:59:55"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/dev/api/responses/chapters-list.json b/dev/api/responses/chapters-list.json
new file mode 100644 (file)
index 0000000..0c1fc5f
--- /dev/null
@@ -0,0 +1,29 @@
+{
+  "data": [
+    {
+      "id": 1,
+      "book_id": 1,
+      "name": "Content Creation",
+      "slug": "content-creation",
+      "description": "How to create documentation on whatever subject you need to write about.",
+      "priority": 3,
+      "created_at": "2019-05-05 21:49:56",
+      "updated_at": "2019-09-28 11:24:23",
+      "created_by": 1,
+      "updated_by": 1
+    },
+    {
+      "id": 2,
+      "book_id": 1,
+      "name": "Managing Content",
+      "slug": "managing-content",
+      "description": "How to keep things organised and orderly in the system for easier navigation and better user experience.",
+      "priority": 5,
+      "created_at": "2019-05-05 21:58:07",
+      "updated_at": "2019-10-17 15:05:34",
+      "created_by": 3,
+      "updated_by": 3
+    }
+  ],
+  "total": 40
+}
\ No newline at end of file
diff --git a/dev/api/responses/chapters-read.json b/dev/api/responses/chapters-read.json
new file mode 100644 (file)
index 0000000..2eddad8
--- /dev/null
@@ -0,0 +1,59 @@
+{
+  "id": 1,
+  "book_id": 1,
+  "slug": "content-creation",
+  "name": "Content Creation",
+  "description": "How to create documentation on whatever subject you need to write about.",
+  "priority": 3,
+  "created_at": "2019-05-05 21:49:56",
+  "updated_at": "2019-09-28 11:24:23",
+  "created_by": {
+    "id": 1,
+    "name": "Admin"
+  },
+  "updated_by": {
+    "id": 1,
+    "name": "Admin"
+  },
+  "tags": [
+    {
+      "name": "Category",
+      "value": "Guide",
+      "order": 0,
+      "created_at": "2020-05-22 22:51:51",
+      "updated_at": "2020-05-22 22:51:51"
+    }
+  ],
+  "pages": [
+    {
+      "id": 1,
+      "book_id": 1,
+      "chapter_id": 1,
+      "name": "How to create page content",
+      "slug": "how-to-create-page-content",
+      "priority": 0,
+      "created_at": "2019-05-05 21:49:58",
+      "updated_at": "2019-08-26 14:32:59",
+      "created_by": 1,
+      "updated_by": 1,
+      "draft": 0,
+      "revision_count": 2,
+      "template": 0
+    },
+    {
+      "id": 7,
+      "book_id": 1,
+      "chapter_id": 1,
+      "name": "Good book structure",
+      "slug": "good-book-structure",
+      "priority": 1,
+      "created_at": "2019-05-05 22:01:55",
+      "updated_at": "2019-06-06 12:03:04",
+      "created_by": 3,
+      "updated_by": 3,
+      "draft": 0,
+      "revision_count": 1,
+      "template": 0
+    }
+  ]
+}
\ No newline at end of file
diff --git a/dev/api/responses/chapters-update.json b/dev/api/responses/chapters-update.json
new file mode 100644 (file)
index 0000000..a7edb15
--- /dev/null
@@ -0,0 +1,38 @@
+{
+  "id": 75,
+  "book_id": 1,
+  "slug": "my-fantastic-updated-chapter",
+  "name": "My fantastic updated chapter",
+  "description": "This is an updated chapter that I've altered via the API",
+  "priority": 7,
+  "created_at": "2020-05-22 23:03:35",
+  "updated_at": "2020-05-22 23:07:20",
+  "created_by": 1,
+  "updated_by": 1,
+  "book": {
+    "id": 1,
+    "name": "BookStack User Guide",
+    "slug": "bookstack-user-guide",
+    "description": "This is a general guide on using BookStack on a day-to-day basis.",
+    "created_at": "2019-05-05 21:48:46",
+    "updated_at": "2019-12-11 20:57:31",
+    "created_by": 1,
+    "updated_by": 1
+  },
+  "tags": [
+    {
+      "name": "Category",
+      "value": "Kinda Good Content",
+      "order": 0,
+      "created_at": "2020-05-22 23:07:20",
+      "updated_at": "2020-05-22 23:07:20"
+    },
+    {
+      "name": "Rating",
+      "value": "Medium",
+      "order": 0,
+      "created_at": "2020-05-22 23:07:20",
+      "updated_at": "2020-05-22 23:07:20"
+    }
+  ]
+}
\ No newline at end of file
index 8a8e2348b2f78df5f5eb48b78544867b35f657a6..634fbb5a53c6fde235e72c2516b111a73f645451 100644 (file)
@@ -5,15 +5,12 @@
   "description": "This is my shelf with some books",
   "created_by": {
     "id": 1,
-    "name": "Admin",
-    "image_id": 48
+    "name": "Admin"
   },
   "updated_by": {
     "id": 1,
-    "name": "Admin",
-    "image_id": 48
+    "name": "Admin"
   },
-  "image_id": 501,
   "created_at": "2020-04-10 13:24:09",
   "updated_at": "2020-04-10 13:31:04",
   "tags": [
index f9c27b62f29581770b65c0a704ebaf31a8f66324..1b90d9b8fd12d591cf87b6e34fba2191b6711ff9 100644 (file)
@@ -9,18 +9,28 @@
 Route::get('docs', 'ApiDocsController@display');
 Route::get('docs.json', 'ApiDocsController@json');
 
-Route::get('books', 'BooksApiController@list');
-Route::post('books', 'BooksApiController@create');
-Route::get('books/{id}', 'BooksApiController@read');
-Route::put('books/{id}', 'BooksApiController@update');
-Route::delete('books/{id}', 'BooksApiController@delete');
+Route::get('books', 'BookApiController@list');
+Route::post('books', 'BookApiController@create');
+Route::get('books/{id}', 'BookApiController@read');
+Route::put('books/{id}', 'BookApiController@update');
+Route::delete('books/{id}', 'BookApiController@delete');
 
-Route::get('books/{id}/export/html', 'BooksExportApiController@exportHtml');
-Route::get('books/{id}/export/pdf', 'BooksExportApiController@exportPdf');
-Route::get('books/{id}/export/plaintext', 'BooksExportApiController@exportPlainText');
+Route::get('books/{id}/export/html', 'BookExportApiController@exportHtml');
+Route::get('books/{id}/export/pdf', 'BookExportApiController@exportPdf');
+Route::get('books/{id}/export/plaintext', 'BookExportApiController@exportPlainText');
+
+Route::get('chapters', 'ChapterApiController@list');
+Route::post('chapters', 'ChapterApiController@create');
+Route::get('chapters/{id}', 'ChapterApiController@read');
+Route::put('chapters/{id}', 'ChapterApiController@update');
+Route::delete('chapters/{id}', 'ChapterApiController@delete');
+
+Route::get('chapters/{id}/export/html', 'ChapterExportApiController@exportHtml');
+Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf');
+Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText');
 
 Route::get('shelves', 'BookshelfApiController@list');
 Route::post('shelves', 'BookshelfApiController@create');
 Route::get('shelves/{id}', 'BookshelfApiController@read');
 Route::put('shelves/{id}', 'BookshelfApiController@update');
-Route::delete('shelves/{id}', 'BookshelfApiController@delete');
\ No newline at end of file
+Route::delete('shelves/{id}', 'BookshelfApiController@delete');
diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php
new file mode 100644 (file)
index 0000000..15a4445
--- /dev/null
@@ -0,0 +1,186 @@
+<?php namespace Tests\Api;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Chapter;
+use Tests\TestCase;
+
+class ChaptersApiTest extends TestCase
+{
+    use TestsApi;
+
+    protected $baseEndpoint = '/api/chapters';
+
+    public function test_index_endpoint_returns_expected_chapter()
+    {
+        $this->actingAsApiEditor();
+        $firstChapter = Chapter::query()->orderBy('id', 'asc')->first();
+
+        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
+        $resp->assertJson(['data' => [
+            [
+                'id' => $firstChapter->id,
+                'name' => $firstChapter->name,
+                'slug' => $firstChapter->slug,
+                'book_id' => $firstChapter->book->id,
+                'priority' => $firstChapter->priority,
+            ]
+        ]]);
+    }
+
+    public function test_create_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::query()->first();
+        $details = [
+            'name' => 'My API chapter',
+            'description' => 'A chapter created via the API',
+            'book_id' => $book->id,
+            'tags' => [
+                [
+                    'name' => 'tagname',
+                    'value' => 'tagvalue',
+                ]
+            ]
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        $resp->assertStatus(200);
+        $newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
+        $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
+        $this->assertDatabaseHas('tags', [
+            'entity_id' => $newItem->id,
+            'entity_type' => $newItem->getMorphClass(),
+            'name' => 'tagname',
+            'value' => 'tagvalue',
+        ]);
+        $resp->assertJsonMissing(['pages' => []]);
+        $this->assertActivityExists('chapter_create', $newItem);
+    }
+
+    public function test_chapter_name_needed_to_create()
+    {
+        $this->actingAsApiEditor();
+        $book = Book::query()->first();
+        $details = [
+            'book_id' => $book->id,
+            'description' => 'A chapter created via the API',
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        $resp->assertStatus(422);
+        $resp->assertJson($this->validationResponse([
+            "name" => ["The name field is required."]
+        ]));
+    }
+
+    public function test_chapter_book_id_needed_to_create()
+    {
+        $this->actingAsApiEditor();
+        $details = [
+            'name' => 'My api chapter',
+            'description' => 'A chapter created via the API',
+        ];
+
+        $resp = $this->postJson($this->baseEndpoint, $details);
+        $resp->assertStatus(422);
+        $resp->assertJson($this->validationResponse([
+            "book_id" => ["The book id field is required."]
+        ]));
+    }
+
+    public function test_read_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $chapter = Chapter::visible()->first();
+        $page = $chapter->pages()->first();
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$chapter->id}");
+        $resp->assertStatus(200);
+        $resp->assertJson([
+            'id' => $chapter->id,
+            'slug' => $chapter->slug,
+            'created_by' => [
+                'name' => $chapter->createdBy->name,
+            ],
+            'book_id' => $chapter->book_id,
+            'updated_by' => [
+                'name' => $chapter->createdBy->name,
+            ],
+            'pages' => [
+                [
+                    'id' => $page->id,
+                    'slug' => $page->slug,
+                    'name' => $page->name,
+                ]
+            ],
+        ]);
+        $resp->assertJsonCount($chapter->pages()->count(), 'pages');
+    }
+
+    public function test_update_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $chapter = Chapter::visible()->first();
+        $details = [
+            'name' => 'My updated API chapter',
+            'description' => 'A chapter created via the API',
+            'tags' => [
+                [
+                    'name' => 'freshtag',
+                    'value' => 'freshtagval',
+                ]
+            ],
+        ];
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$chapter->id}", $details);
+        $chapter->refresh();
+
+        $resp->assertStatus(200);
+        $resp->assertJson(array_merge($details, [
+            'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id
+        ]));
+        $this->assertActivityExists('chapter_update', $chapter);
+    }
+
+    public function test_delete_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $chapter = Chapter::visible()->first();
+        $resp = $this->deleteJson($this->baseEndpoint . "/{$chapter->id}");
+
+        $resp->assertStatus(204);
+        $this->assertActivityExists('chapter_delete');
+    }
+
+    public function test_export_html_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $chapter = Chapter::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/html");
+        $resp->assertStatus(200);
+        $resp->assertSee($chapter->name);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
+    }
+
+    public function test_export_plain_text_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $chapter = Chapter::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/plaintext");
+        $resp->assertStatus(200);
+        $resp->assertSee($chapter->name);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
+    }
+
+    public function test_export_pdf_endpoint()
+    {
+        $this->actingAsApiEditor();
+        $chapter = Chapter::visible()->first();
+
+        $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/pdf");
+        $resp->assertStatus(200);
+        $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
+    }
+}
\ No newline at end of file
index 623fa6969a7128bf9652f9e50e16fa710d29ac54..1ad4d14b64e4c7137134552848b1f7c800dbed6f 100644 (file)
@@ -23,6 +23,16 @@ trait TestsApi
         return ["error" => ["code" => $code, "message" => $message]];
     }
 
+    /**
+     * Format the given (field_name => ["messages"]) array
+     * into a standard validation response format.
+     */
+    protected function validationResponse(array $messages): array
+    {
+        $err = $this->errorResponse("The given data was invalid.", 422);
+        $err['error']['validation'] = $messages;
+        return $err;
+    }
     /**
      * Get an approved API auth header.
      */