diff --git a/.gitignore b/.gitignore index 081dddebd..15c88373a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ html output docs/node_modules docs/.vuepress/dist +.phpunit.result.cache diff --git a/routes/api.php b/routes/api.php index ed6150abb..1c4808627 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,4 +4,5 @@ Route::post('/{repository}', 'RepositoryStoreController@handle'); Route::get('/{repository}/{repositoryId}', 'RepositoryShowController@handle'); Route::patch('/{repository}/{repositoryId}', 'RepositoryUpdateController@handle'); +Route::put('/{repository}/{repositoryId}', 'RepositoryUpdateController@handle'); Route::delete('/{repository}/{repositoryId}', 'RepositoryDestroyController@handle'); diff --git a/src/Exceptions/RestifyHandler.php b/src/Exceptions/RestifyHandler.php index 233d9c9f4..c8b1ec8b0 100644 --- a/src/Exceptions/RestifyHandler.php +++ b/src/Exceptions/RestifyHandler.php @@ -9,7 +9,6 @@ use Binaryk\LaravelRestify\Exceptions\UnauthorizedException as ActionUnauthorizedException; use Binaryk\LaravelRestify\Restify; use Closure; -use Exception; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -54,8 +53,8 @@ class RestifyHandler extends ExceptionHandler /** * Render an exception into an HTTP response. * - * @param Request $request - * @param \Exception|Throwable $exception + * @param Request $request + * @param \Exception|Throwable $exception * * @return Response|\Symfony\Component\HttpFoundation\Response * @throws \Illuminate\Contracts\Container\BindingResolutionException @@ -94,13 +93,13 @@ public function render($request, $exception) case $exception instanceof ValidationUnauthorized: case $exception instanceof UnauthorizedHttpException: case $exception instanceof UnauthenticateException: - case $exception instanceof ActionUnauthorizedException: - case $exception instanceof AuthorizationException: case $exception instanceof GatePolicy: case $exception instanceof AuthenticationException: $response->addError($exception->getMessage())->auth(); break; + case $exception instanceof AuthorizationException: + case $exception instanceof ActionUnauthorizedException: case $exception instanceof AccessDeniedHttpException: case $exception instanceof InvalidSignatureException: $response->addError($exception->getMessage())->forbidden(); @@ -121,7 +120,7 @@ public function render($request, $exception) /** * Report or log an exception. * - * @param \Exception|Throwable $e + * @param \Exception|Throwable $e * @return mixed * * @throws \Exception diff --git a/src/Fields/Field.php b/src/Fields/Field.php index 85bc20742..29173609b 100644 --- a/src/Fields/Field.php +++ b/src/Fields/Field.php @@ -153,7 +153,7 @@ public function getAttribute() /** * Validation rules for store. - * @param callable|array|string $rules + * @param $rules * @return Field */ public function storingRules($rules) @@ -166,7 +166,7 @@ public function storingRules($rules) /** * Validation rules for update. * - * @param callable|array|string $rules + * @param $rules * @return Field */ public function updatingRules($rules) @@ -178,7 +178,7 @@ public function updatingRules($rules) /** * Validation rules for store. - * @param callable|array|string $rules + * @param $rules * @return Field */ public function rules($rules) diff --git a/tests/Controllers/RepositoryDestroyControllerTest.php b/tests/Controllers/RepositoryDestroyControllerTest.php new file mode 100644 index 000000000..f229dab53 --- /dev/null +++ b/tests/Controllers/RepositoryDestroyControllerTest.php @@ -0,0 +1,55 @@ + + */ +class RepositoryDestroyControllerTest extends IntegrationTest +{ + protected function setUp(): void + { + parent::setUp(); + $this->authenticate(); + $this->app->bind(ExceptionHandler::class, RestifyHandler::class); + } + + public function test_destroy_works() + { + $post = factory(Post::class)->create(['user_id' => 1]); + + $this->assertInstanceOf(Post::class, Post::find($post->id)); + + $this->withoutExceptionHandling()->delete('/restify-api/posts/'.$post->id, [ + 'title' => 'Updated title', + ]) + ->assertStatus(204); + + $this->assertNull(Post::find($post->id)); + } + + public function test_unathorized_to_destroy() + { + Gate::policy(Post::class, PostPolicy::class); + + $post = factory(Post::class)->create(['user_id' => 1]); + + $_SERVER['restify.post.deletable'] = false; + + $this->delete('/restify-api/posts/'.$post->id, [ + 'title' => 'Updated title', + ])->assertStatus(403) + ->assertJson([ + 'errors' => ['This action is unauthorized.'], + ]); + + $this->assertInstanceOf(Post::class, $post->refresh()); + } +} diff --git a/tests/Controllers/RepositoryUpdateControllerTest.php b/tests/Controllers/RepositoryUpdateControllerTest.php new file mode 100644 index 000000000..625938b59 --- /dev/null +++ b/tests/Controllers/RepositoryUpdateControllerTest.php @@ -0,0 +1,68 @@ + + */ +class RepositoryUpdateControllerTest extends IntegrationTest +{ + protected function setUp(): void + { + parent::setUp(); + $this->authenticate(); + } + + public function test_basic_update_works() + { + $post = factory(Post::class)->create(['user_id' => 1]); + + $this->withoutExceptionHandling()->patch('/restify-api/posts/'.$post->id, [ + 'title' => 'Updated title', + ]) + ->assertStatus(200); + + $updatedPost = Post::find($post->id); + + $this->assertEquals($updatedPost->title, 'Updated title'); + } + + public function test_put_works() + { + $post = factory(Post::class)->create(['user_id' => 1]); + + $this->withoutExceptionHandling()->put('/restify-api/posts/'.$post->id, [ + 'title' => 'Updated title', + ]) + ->assertStatus(200); + + $updatedPost = Post::find($post->id); + + $this->assertEquals($updatedPost->title, 'Updated title'); + } + + public function test_unathorized_to_update() + { + $this->app->bind(ExceptionHandler::class, RestifyHandler::class); + + Gate::policy(Post::class, PostPolicy::class); + + $post = factory(Post::class)->create(['user_id' => 1]); + + $_SERVER['restify.post.updateable'] = false; + + $this->patch('/restify-api/posts/'.$post->id, [ + 'title' => 'Updated title', + ])->assertStatus(403) + ->assertJson([ + 'errors' => ['This action is unauthorized.'], + ]); + } +} diff --git a/tests/Fixtures/PostPolicy.php b/tests/Fixtures/PostPolicy.php index 85c78af7d..161b1a02e 100644 --- a/tests/Fixtures/PostPolicy.php +++ b/tests/Fixtures/PostPolicy.php @@ -22,4 +22,14 @@ public function store($user) { return $_SERVER['restify.post.creatable'] ?? true; } + + public function update($user, $post) + { + return $_SERVER['restify.post.updateable'] ?? true; + } + + public function delete($user, $post) + { + return $_SERVER['restify.post.deletable'] ?? true; + } } diff --git a/tests/HandlerTest.php b/tests/HandlerTest.php index 1def7bac5..aca2d87a8 100644 --- a/tests/HandlerTest.php +++ b/tests/HandlerTest.php @@ -7,6 +7,7 @@ use Binaryk\LaravelRestify\Exceptions\Guard\GatePolicy; use Binaryk\LaravelRestify\Exceptions\RestifyHandler; use Binaryk\LaravelRestify\Exceptions\UnauthenticateException; +use Binaryk\LaravelRestify\Restify; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Testing\Concerns\InteractsWithContainer; @@ -150,4 +151,16 @@ public function test_default_unhandled_exception_production() $this->assertObjectHasAttribute('errors', $response->getData()); $this->assertEquals($response->getData()->errors[0], __('messages.something_went_wrong')); } + + public function test_can_inject_custom_handler_but_handler_will_continue_handle() + { + Restify::exceptionHandler(function ($request, $exception) { + $this->assertInstanceOf(NotFoundHttpException::class, $exception); + }); + + $response = $this->handler->render($this->request, new NotFoundHttpException('This message is not visible')); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals($response->getData()->errors[0], __('messages.not_found')); + $this->assertEquals($response->getStatusCode(), 404); + } } diff --git a/tests/RepositoryWithRoutesTest.php b/tests/RepositoryWithRoutesTest.php index 53c0f0503..b433df6ae 100644 --- a/tests/RepositoryWithRoutesTest.php +++ b/tests/RepositoryWithRoutesTest.php @@ -21,6 +21,7 @@ protected function setUp(): void WithCustomPrefix::class, WithCustomMiddleware::class, WithCustomNamespace::class, + WithoutGroup::class, ]); parent::setUp(); @@ -62,6 +63,17 @@ public function test_can_use_custom_namespace() ], ]); } + + public function test_routes_default_wrapped() + { + $this->withoutExceptionHandling()->getJson(route('no.group.default.options')) + ->assertStatus(200) + ->assertJson([ + 'meta' => [ + 'message' => 'From the sayHello method', + ], + ]); + } } class RepositoryWithRoutes extends Repository @@ -69,8 +81,9 @@ class RepositoryWithRoutes extends Repository /** * @param Router $router * @param array $attributes + * @param bool $wrap */ - public static function routes(Router $router, $attributes) + public static function routes(Router $router, $attributes, $wrap = false) { $router->group($attributes, function ($router) { $router->get('/main-testing', function () { @@ -89,7 +102,7 @@ public static function uriKey() class WithCustomPrefix extends RepositoryWithRoutes { - public static function routes(Router $router, $attributes) + public static function routes(Router $router, $attributes, $wrap = false) { $attributes['prefix'] = 'custom-prefix'; @@ -115,7 +128,7 @@ public function handle($request, $next) class WithCustomMiddleware extends RepositoryWithRoutes { - public static function routes(Router $router, $options) + public static function routes(Router $router, $options, $wrap = false) { $options['middleware'] = [MiddlewareFail::class]; @@ -131,7 +144,7 @@ public static function routes(Router $router, $options) class WithCustomNamespace extends RepositoryWithRoutes { - public static function routes(Router $router, $options) + public static function routes(Router $router, $options, $wrap = false) { $options['namespace'] = 'Binaryk\LaravelRestify\Tests'; @@ -141,6 +154,14 @@ public static function routes(Router $router, $options) } } +class WithoutGroup extends RepositoryWithRoutes +{ + public static function routes(Router $router, $options = [], $wrap = true) + { + $router->get('default-options', '\\'.HandleController::class.'@sayHello')->name('no.group.default.options'); + } +} + class HandleController extends RestController { /**