mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-23 11:22:33 +01:00
Filled out base Book API endpoints, added example responses
This commit is contained in:
parent
a8595d8aaf
commit
04a8614136
@ -47,7 +47,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
* The attributes excluded from the model's JSON form.
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
protected $hidden = ['password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email'];
|
||||
|
||||
/**
|
||||
* This holds the user's permissions when loaded.
|
||||
|
@ -18,7 +18,8 @@ class Book extends Entity implements HasCoverImage
|
||||
{
|
||||
public $searchFactor = 2;
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
protected $fillable = ['name', 'description'];
|
||||
protected $hidden = ['restricted'];
|
||||
|
||||
/**
|
||||
* Get the url for this book.
|
||||
|
@ -8,6 +8,8 @@ use Illuminate\Http\JsonResponse;
|
||||
class ApiController extends Controller
|
||||
{
|
||||
|
||||
protected $rules = [];
|
||||
|
||||
/**
|
||||
* Provide a paginated listing JSON response in a standard format
|
||||
* taking into account any pagination parameters passed by the user.
|
||||
@ -17,4 +19,12 @@ class ApiController extends Controller
|
||||
$listing = new ListingResponseBuilder($query, request(), $fields);
|
||||
return $listing->toResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules for this controller.
|
||||
*/
|
||||
public function getValdationRules(): array
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
}
|
@ -1,47 +1,99 @@
|
||||
<?php namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Facades\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BooksApiController extends ApiController
|
||||
{
|
||||
public $validation = [
|
||||
|
||||
protected $bookRepo;
|
||||
|
||||
protected $rules = [
|
||||
'create' => [
|
||||
// TODO
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
],
|
||||
'update' => [
|
||||
// TODO
|
||||
'name' => 'string|min:1|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* BooksApiController constructor.
|
||||
*/
|
||||
public function __construct(BookRepo $bookRepo)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a listing of books visible to the user.
|
||||
* @api listing
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$books = Book::visible();
|
||||
return $this->apiListingResponse($books, [
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by',
|
||||
'restricted', 'image_id',
|
||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
/**
|
||||
* Create a new book.
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
// TODO -
|
||||
$this->checkPermission('book-create-all');
|
||||
$requestData = $this->validate($request, $this->rules['create']);
|
||||
|
||||
$book = $this->bookRepo->create($requestData);
|
||||
Activity::add($book, 'book_create', $book->id);
|
||||
|
||||
return response()->json($book);
|
||||
}
|
||||
|
||||
public function read()
|
||||
/**
|
||||
* View the details of a single book.
|
||||
*/
|
||||
public function read(string $id)
|
||||
{
|
||||
// TODO -
|
||||
$book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy'])->findOrFail($id);
|
||||
return response()->json($book);
|
||||
}
|
||||
|
||||
public function update()
|
||||
/**
|
||||
* Update the details of a single book.
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
// TODO -
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
$book = $this->bookRepo->update($book, $requestData);
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
|
||||
return response()->json($book);
|
||||
}
|
||||
|
||||
public function delete()
|
||||
/**
|
||||
* Delete a book from the system.
|
||||
* @throws \BookStack\Exceptions\NotifyException
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
*/
|
||||
public function delete(string $id)
|
||||
{
|
||||
// TODO -
|
||||
$book = Book::visible()->findOrFail($id);
|
||||
$this->checkOwnablePermission('book-delete', $book);
|
||||
|
||||
$this->bookRepo->destroy($book);
|
||||
Activity::addMessage('book_delete', $book->name);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ class Image extends Ownable
|
||||
{
|
||||
|
||||
protected $fillable = ['name'];
|
||||
protected $hidden = [];
|
||||
|
||||
/**
|
||||
* Get a thumbnail for this image.
|
||||
|
10
dev/api/responses/books-create.json
Normal file
10
dev/api/responses/books-create.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "My new book",
|
||||
"description": "This is a book created via the API",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"slug": "my-new-book",
|
||||
"updated_at": "2020-01-12 14:05:11",
|
||||
"created_at": "2020-01-12 14:05:11",
|
||||
"id": 15
|
||||
}
|
27
dev/api/responses/books-index.json
Normal file
27
dev/api/responses/books-index.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"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,
|
||||
"image_id": 3
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Inventore inventore quia voluptatem.",
|
||||
"slug": "inventore-inventore-quia-voluptatem",
|
||||
"description": "Veniam nihil voluptas enim laborum corporis quos sint. Ab rerum voluptas ut iste voluptas magni quibusdam ut. Amet omnis enim voluptate neque facilis.",
|
||||
"created_at": "2019-05-05 22:10:14",
|
||||
"updated_at": "2019-12-11 20:57:23",
|
||||
"created_by": 4,
|
||||
"updated_by": 3,
|
||||
"image_id": 34
|
||||
}
|
||||
],
|
||||
"total": 14
|
||||
}
|
47
dev/api/responses/books-read.json
Normal file
47
dev/api/responses/books-read.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"id": 16,
|
||||
"name": "My own book",
|
||||
"slug": "my-own-book",
|
||||
"description": "This is my own little book",
|
||||
"created_at": "2020-01-12 14:09:59",
|
||||
"updated_at": "2020-01-12 14:11:51",
|
||||
"created_by": {
|
||||
"id": 1,
|
||||
"name": "Admin",
|
||||
"created_at": "2019-05-05 21:15:13",
|
||||
"updated_at": "2019-12-16 12:18:37",
|
||||
"image_id": 48
|
||||
},
|
||||
"updated_by": {
|
||||
"id": 1,
|
||||
"name": "Admin",
|
||||
"created_at": "2019-05-05 21:15:13",
|
||||
"updated_at": "2019-12-16 12:18:37",
|
||||
"image_id": 48
|
||||
},
|
||||
"image_id": 452,
|
||||
"tags": [
|
||||
{
|
||||
"id": 13,
|
||||
"entity_id": 16,
|
||||
"entity_type": "BookStack\\Book",
|
||||
"name": "Category",
|
||||
"value": "Guide",
|
||||
"order": 0,
|
||||
"created_at": "2020-01-12 14:11:51",
|
||||
"updated_at": "2020-01-12 14:11:51"
|
||||
}
|
||||
],
|
||||
"cover": {
|
||||
"id": 452,
|
||||
"name": "sjovall_m117hUWMu40.jpg",
|
||||
"url": "http:\/\/bookstack.local\/uploads\/images\/cover_book\/2020-01\/sjovall_m117hUWMu40.jpg",
|
||||
"created_at": "2020-01-12 14:11:51",
|
||||
"updated_at": "2020-01-12 14:11:51",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"path": "\/uploads\/images\/cover_book\/2020-01\/sjovall_m117hUWMu40.jpg",
|
||||
"type": "cover_book",
|
||||
"uploaded_to": 16
|
||||
}
|
||||
}
|
11
dev/api/responses/books-update.json
Normal file
11
dev/api/responses/books-update.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": 16,
|
||||
"name": "My own book",
|
||||
"slug": "my-own-book",
|
||||
"description": "This is my own little book - updated",
|
||||
"created_at": "2020-01-12 14:09:59",
|
||||
"updated_at": "2020-01-12 14:16:10",
|
||||
"created_by": 1,
|
||||
"updated_by": 1,
|
||||
"image_id": 452
|
||||
}
|
@ -2,11 +2,12 @@
|
||||
|
||||
/**
|
||||
* Routes for the BookStack API.
|
||||
*
|
||||
* Routes have a uri prefix of /api/.
|
||||
* Controllers are all within app/Http/Controllers/Api
|
||||
*/
|
||||
|
||||
|
||||
// TODO - Authenticate middleware
|
||||
|
||||
Route::get('books', 'BooksApiController@index');
|
||||
Route::get('books', 'BooksApiController@index');
|
||||
Route::post('books', 'BooksApiController@create');
|
||||
Route::get('books/{id}', 'BooksApiController@read');
|
||||
Route::put('books/{id}', 'BooksApiController@update');
|
||||
Route::delete('books/{id}', 'BooksApiController@delete');
|
||||
|
87
tests/Api/BooksApiTest.php
Normal file
87
tests/Api/BooksApiTest.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Entities\Book;
|
||||
|
||||
class ApiAuthTest extends TestCase
|
||||
{
|
||||
use TestsApi;
|
||||
|
||||
protected $baseEndpoint = '/api/books';
|
||||
|
||||
public function test_index_endpoint_returns_expected_book()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$firstBook = Book::query()->orderBy('id', 'asc')->first();
|
||||
|
||||
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
|
||||
$resp->assertJson(['data' => [
|
||||
[
|
||||
'id' => $firstBook->id,
|
||||
'name' => $firstBook->name,
|
||||
'slug' => $firstBook->slug,
|
||||
]
|
||||
]]);
|
||||
}
|
||||
|
||||
public function test_create_endpoint()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$details = [
|
||||
'name' => 'My API book',
|
||||
'description' => 'A book created via the API',
|
||||
];
|
||||
|
||||
$resp = $this->postJson($this->baseEndpoint, $details);
|
||||
$resp->assertStatus(200);
|
||||
$newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
|
||||
$resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
|
||||
$this->assertActivityExists('book_create', $newItem);
|
||||
}
|
||||
|
||||
public function test_read_endpoint()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$book = Book::visible()->first();
|
||||
|
||||
$resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertJson([
|
||||
'id' => $book->id,
|
||||
'slug' => $book->slug,
|
||||
'created_by' => [
|
||||
'name' => $book->createdBy->name,
|
||||
],
|
||||
'updated_by' => [
|
||||
'name' => $book->createdBy->name,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$book = Book::visible()->first();
|
||||
$details = [
|
||||
'name' => 'My updated API book',
|
||||
'description' => 'A book created via the API',
|
||||
];
|
||||
|
||||
$resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
|
||||
$book->refresh();
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertJson(array_merge($details, ['id' => $book->id, 'slug' => $book->slug]));
|
||||
$this->assertActivityExists('book_update', $book);
|
||||
}
|
||||
|
||||
public function test_delete_endpoint()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$book = Book::visible()->first();
|
||||
$resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
|
||||
|
||||
$resp->assertStatus(204);
|
||||
$this->assertActivityExists('book_delete');
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Entities\Entity;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
@ -60,4 +61,20 @@ abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
return TestResponse::fromBaseResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that an activity entry exists of the given key.
|
||||
* Checks the activity belongs to the given entity if provided.
|
||||
*/
|
||||
protected function assertActivityExists(string $key, Entity $entity = null)
|
||||
{
|
||||
$detailsToCheck = ['key' => $key];
|
||||
|
||||
if ($entity) {
|
||||
$detailsToCheck['entity_type'] = $entity->getMorphClass();
|
||||
$detailsToCheck['entity_id'] = $entity->id;
|
||||
}
|
||||
|
||||
$this->assertDatabaseHas('activities', $detailsToCheck);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user