diff --git a/app/Actions/Tag.php b/app/Actions/Tag.php index 38d0458e4..80a911508 100644 --- a/app/Actions/Tag.php +++ b/app/Actions/Tag.php @@ -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 diff --git a/app/Actions/TagRepo.php b/app/Actions/TagRepo.php index 0cbfa4163..b8b1eb464 100644 --- a/app/Actions/TagRepo.php +++ b/app/Actions/TagRepo.php @@ -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; diff --git a/app/Auth/User.php b/app/Auth/User.php index a581d9993..40718beb6 100644 --- a/app/Auth/User.php +++ b/app/Auth/User.php @@ -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', ]; /** diff --git a/app/Entities/Book.php b/app/Entities/Book.php index 38b7d4a8c..af8344b88 100644 --- a/app/Entities/Book.php +++ b/app/Entities/Book.php @@ -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. diff --git a/app/Entities/Bookshelf.php b/app/Entities/Bookshelf.php index c7ba840e0..474ba51cd 100644 --- a/app/Entities/Bookshelf.php +++ b/app/Entities/Bookshelf.php @@ -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. diff --git a/app/Entities/Chapter.php b/app/Entities/Chapter.php index 848bc6448..3290afcfa 100644 --- a/app/Entities/Chapter.php +++ b/app/Entities/Chapter.php @@ -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. diff --git a/app/Entities/Entity.php b/app/Entities/Entity.php index 5013c39cf..6a5894cac 100644 --- a/app/Entities/Entity.php +++ b/app/Entities/Entity.php @@ -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); } /** diff --git a/app/Entities/Page.php b/app/Entities/Page.php index 76dc628fb..d10786dda 100644 --- a/app/Entities/Page.php +++ b/app/Entities/Page.php @@ -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. */ diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index e49eeb1ef..d92085a61 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -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 = ''; diff --git a/app/Http/Controllers/Api/BooksApiController.php b/app/Http/Controllers/Api/BookApiController.php similarity index 96% rename from app/Http/Controllers/Api/BooksApiController.php rename to app/Http/Controllers/Api/BookApiController.php index ac4ea171c..8333eba3a 100644 --- a/app/Http/Controllers/Api/BooksApiController.php +++ b/app/Http/Controllers/Api/BookApiController.php @@ -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', ], ]; diff --git a/app/Http/Controllers/Api/BooksExportApiController.php b/app/Http/Controllers/Api/BookExportApiController.php similarity index 96% rename from app/Http/Controllers/Api/BooksExportApiController.php rename to app/Http/Controllers/Api/BookExportApiController.php index 605f8f408..31fe5250f 100644 --- a/app/Http/Controllers/Api/BooksExportApiController.php +++ b/app/Http/Controllers/Api/BookExportApiController.php @@ -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 index 000000000..50aa8834e --- /dev/null +++ b/app/Http/Controllers/Api/ChapterApiController.php @@ -0,0 +1,104 @@ + [ + '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 index 000000000..f19f29e9d --- /dev/null +++ b/app/Http/Controllers/Api/ChapterExportApiController.php @@ -0,0 +1,54 @@ +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 index 000000000..ca06fc298 --- /dev/null +++ b/dev/api/requests/chapters-create.json @@ -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 index 000000000..6bd3a3e5c --- /dev/null +++ b/dev/api/requests/chapters-update.json @@ -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 diff --git a/dev/api/responses/books-read.json b/dev/api/responses/books-read.json index 11408e9ab..2e43f5f87 100644 --- a/dev/api/responses/books-read.json +++ b/dev/api/responses/books-read.json @@ -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 index 000000000..7aac27687 --- /dev/null +++ b/dev/api/responses/chapters-create.json @@ -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 index 000000000..0c1fc5fc2 --- /dev/null +++ b/dev/api/responses/chapters-list.json @@ -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 index 000000000..2eddad895 --- /dev/null +++ b/dev/api/responses/chapters-read.json @@ -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 index 000000000..a7edb15b0 --- /dev/null +++ b/dev/api/responses/chapters-update.json @@ -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 diff --git a/dev/api/responses/shelves-read.json b/dev/api/responses/shelves-read.json index 8a8e2348b..634fbb5a5 100644 --- a/dev/api/responses/shelves-read.json +++ b/dev/api/responses/shelves-read.json @@ -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": [ diff --git a/routes/api.php b/routes/api.php index f9c27b62f..1b90d9b8f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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 index 000000000..15a44459e --- /dev/null +++ b/tests/Api/ChaptersApiTest.php @@ -0,0 +1,186 @@ +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 diff --git a/tests/Api/TestsApi.php b/tests/Api/TestsApi.php index 623fa6969..1ad4d14b6 100644 --- a/tests/Api/TestsApi.php +++ b/tests/Api/TestsApi.php @@ -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. */