diff --git a/src/Concerns/HandlesRelationManyToManyOperations.php b/src/Concerns/HandlesRelationManyToManyOperations.php index 70680b71..a8b11346 100644 --- a/src/Concerns/HandlesRelationManyToManyOperations.php +++ b/src/Concerns/HandlesRelationManyToManyOperations.php @@ -17,11 +17,13 @@ trait HandlesRelationManyToManyOperations * Attach resource to the relation in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array$args * @return JsonResponse */ - public function attach(Request $request, $parentKey) + public function attach(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->attachWithTransaction($request, $parentKey); @@ -237,11 +239,13 @@ protected function afterAttach(Request $request, Model $parentEntity, array &$at * Detach resource to the relation in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return JsonResponse */ - public function detach(Request $request, $parentKey) + public function detach(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->detachWithTransaction($request, $parentKey); @@ -360,11 +364,13 @@ protected function afterDetach(Request $request, Model $parentEntity, array &$de * Sync relation resources in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return JsonResponse */ - public function sync(Request $request, $parentKey) + public function sync(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->syncWithTransaction($request, $parentKey); @@ -486,11 +492,13 @@ protected function afterSync(Request $request, Model $parentEntity, array &$sync * Toggle relation resources in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return JsonResponse */ - public function toggle(Request $request, $parentKey) + public function toggle(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->toggleWithTransaction($request, $parentKey); @@ -603,12 +611,14 @@ protected function afterToggle(Request $request, Model $parentEntity, array &$to * Update relation resource pivot in a transaction-safe wqy. * * @param Request $request - * @param int|string $parentKey - * @param int|string $relatedKey + * @param array $args * @return JsonResponse */ - public function updatePivot(Request $request, $parentKey, $relatedKey) + public function updatePivot(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $relatedKey = $this->keyResolver->resolveRelationOperationRelatedKey($request, $args); + try { $this->startTransaction(); $result = $this->updatePivotWithTransaction($request, $parentKey, $relatedKey); diff --git a/src/Concerns/HandlesRelationOneToManyOperations.php b/src/Concerns/HandlesRelationOneToManyOperations.php index facc9fd2..29f4c5f6 100644 --- a/src/Concerns/HandlesRelationOneToManyOperations.php +++ b/src/Concerns/HandlesRelationOneToManyOperations.php @@ -14,11 +14,13 @@ trait HandlesRelationOneToManyOperations * Associates resource with another resource in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return Resource */ - public function associate(Request $request, $parentKey) + public function associate(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->associateWithTransaction($request, $parentKey); @@ -164,12 +166,14 @@ protected function afterAssociate(Request $request, Model $parentEntity, Model $ * Disassociates resource from another resource in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey - * @param int|string $relatedKey + * @param array $args * @return Resource */ - public function dissociate(Request $request, $parentKey, $relatedKey) + public function dissociate(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $relatedKey = $this->keyResolver->resolveRelationOperationRelatedKey($request, $args); + try { $this->startTransaction(); $result = $this->dissociateWithTransaction($request, $parentKey, $relatedKey); diff --git a/src/Concerns/HandlesRelationStandardBatchOperations.php b/src/Concerns/HandlesRelationStandardBatchOperations.php index ec8b3644..6a7b5990 100644 --- a/src/Concerns/HandlesRelationStandardBatchOperations.php +++ b/src/Concerns/HandlesRelationStandardBatchOperations.php @@ -17,11 +17,13 @@ trait HandlesRelationStandardBatchOperations * Create a batch of new relation resources in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return CollectionResource */ - public function batchStore(Request $request, $parentKey) + public function batchStore(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->batchStoreWithTransaction($request, $parentKey); @@ -161,11 +163,13 @@ protected function afterBatchStore(Request $request, Model $parentEntity, Collec * Updates a batch of relation resources in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return CollectionResource */ - public function batchUpdate(Request $request, $parentKey) + public function batchUpdate(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->batchUpdateWithTransaction($request, $parentKey); @@ -355,12 +359,14 @@ protected function afterBatchUpdate(Request $request, Model $parentEntity, Colle * Deletes a batch of relation resources in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return CollectionResource * @throws Exception */ - public function batchDestroy(Request $request, $parentKey) + public function batchDestroy(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->batchDestroyWithTransaction($request, $parentKey); @@ -527,12 +533,14 @@ protected function afterBatchDestroy(Request $request, Model $parentEntity, Coll * Restores a batch of relation resources in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return CollectionResource * @throws Exception */ - public function batchRestore(Request $request, $parentKey) + public function batchRestore(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->batchRestoreWithTransaction($request, $parentKey); diff --git a/src/Concerns/HandlesRelationStandardOperations.php b/src/Concerns/HandlesRelationStandardOperations.php index 5c65763b..a01dd4db 100644 --- a/src/Concerns/HandlesRelationStandardOperations.php +++ b/src/Concerns/HandlesRelationStandardOperations.php @@ -27,13 +27,15 @@ trait HandlesRelationStandardOperations * Fetch the list of relation resources. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return CollectionResource */ - public function index(Request $request, $parentKey) + public function index(Request $request, ...$args) { $requestedRelations = $this->relationsResolver->requestedRelations($request); + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $parentQuery = $this->buildIndexParentFetchQuery($request, $parentKey); $parentEntity = $this->runIndexParentFetchQuery($request, $parentQuery, $parentKey); @@ -255,11 +257,13 @@ public function search(Request $request, $parentKey) * Create new relation resource. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @return Resource */ - public function store(Request $request, $parentKey) + public function store(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + try { $this->startTransaction(); $result = $this->storeWithTransaction($request, $parentKey); @@ -484,12 +488,14 @@ protected function afterStore(Request $request, Model $parentEntity, Model $enti * Fetch a relation resource. * * @param Request $request - * @param int|string $parentKey - * @param int|string|null $relatedKey + * @param array $args * @return Resource */ - public function show(Request $request, $parentKey, $relatedKey = null) + public function show(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $relatedKey = $this->keyResolver->resolveRelationOperationRelatedKey($request, $args); + $parentQuery = $this->buildShowParentFetchQuery($request, $parentKey); $parentEntity = $this->runShowParentFetchQuery($request, $parentQuery, $parentKey); @@ -672,12 +678,15 @@ protected function afterShow(Request $request, Model $parentEntity, Model $entit * Update a relation resource in a transaction-safe way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @param int|string|null $relatedKey * @return Resource */ - public function update(Request $request, $parentKey, $relatedKey = null) + public function update(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $relatedKey = $this->keyResolver->resolveRelationOperationRelatedKey($request, $args); + try { $this->startTransaction(); $result = $this->updateWithTransaction($request, $parentKey, $relatedKey); @@ -889,13 +898,16 @@ protected function afterUpdate(Request $request, Model $parentEntity, Model $ent * Delete a relation resource. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @param int|string|null $relatedKey * @return Resource * @throws Exception */ - public function destroy(Request $request, $parentKey, $relatedKey = null) + public function destroy(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $relatedKey = $this->keyResolver->resolveRelationOperationRelatedKey($request, $args); + try { $this->startTransaction(); $result = $this->destroyWithTransaction($request, $parentKey, $relatedKey); @@ -1079,12 +1091,15 @@ protected function afterDestroy(Request $request, Model $parentEntity, Model $en * Restores a previously deleted relation resource in a transaction-save way. * * @param Request $request - * @param int|string $parentKey + * @param array $args * @param int|string|null $relatedKey * @return Resource */ - public function restore(Request $request, $parentKey, $relatedKey = null) + public function restore(Request $request, ...$args) { + $parentKey = $this->keyResolver->resolveRelationOperationParentKey($request, $args); + $relatedKey = $this->keyResolver->resolveRelationOperationRelatedKey($request, $args); + try { $this->startTransaction(); $result = $this->restoreWithTransaction($request, $parentKey, $relatedKey); diff --git a/src/Concerns/HandlesStandardOperations.php b/src/Concerns/HandlesStandardOperations.php index 5a36b042..d80525b1 100644 --- a/src/Concerns/HandlesStandardOperations.php +++ b/src/Concerns/HandlesStandardOperations.php @@ -329,17 +329,19 @@ protected function afterStore(Request $request, Model $entity) * Fetches resource. * * @param Request $request - * @param int|string $key + * @param array $args * @return Resource * @throws AuthorizationException * @throws BindingResolutionException */ - public function show(Request $request, $key) + public function show(Request $request, ...$args) { $requestedRelations = $this->relationsResolver->requestedRelations($request); $query = $this->buildShowFetchQuery($request, $requestedRelations); + $key = $this->keyResolver->resolveStandardOperationKey($request, $args); + $beforeHookResult = $this->beforeShow($request, $key); if ($this->hookResponds($beforeHookResult)) { return $beforeHookResult; @@ -438,11 +440,13 @@ protected function afterShow(Request $request, Model $entity) * Update a resource in a transaction-safe way. * * @param Request $request - * @param int|string $key + * @param array $args * @return Resource */ - public function update(Request $request, $key) + public function update(Request $request, ...$args) { + $key = $this->keyResolver->resolveStandardOperationKey($request, $args); + try { $this->startTransaction(); $result = $this->updateWithTransaction($request, $key); @@ -602,12 +606,14 @@ protected function afterUpdate(Request $request, Model $entity) * Deletes a resource. * * @param Request $request - * @param int|string $key + * @param array $args * @return Resource * @throws Exception */ - public function destroy(Request $request, $key) + public function destroy(Request $request, ...$args) { + $key = $this->keyResolver->resolveStandardOperationKey($request, $args); + try { $this->startTransaction(); $result = $this->destroyWithTransaction($request, $key); @@ -767,12 +773,14 @@ protected function afterDestroy(Request $request, Model $entity) * Restore previously deleted resource in a transaction-safe way. * * @param Request $request - * @param int|string $key + * @param array $args * @return Resource * @throws Exception */ - public function restore(Request $request, $key) + public function restore(Request $request, ...$args) { + $key = $this->keyResolver->resolveStandardOperationKey($request, $args); + try { $this->startTransaction(); $result = $this->restoreWithTransaction($request, $key); diff --git a/src/Contracts/KeyResolver.php b/src/Contracts/KeyResolver.php new file mode 100644 index 00000000..870636c1 --- /dev/null +++ b/src/Contracts/KeyResolver.php @@ -0,0 +1,14 @@ + $this instanceof RelationController, ] ); + $this->keyResolver = App::make(KeyResolver::class); $this->resolveComponents(); $this->bindComponents(); @@ -484,6 +491,25 @@ public function setRelationsResolver(RelationsResolver $relationsResolver): self return $this; } + /** + * @return KeyResolver + */ + public function getKeyResolver(): KeyResolver + { + return $this->keyResolver; + } + + /** + * @param KeyResolver $keyResolver + * @return $this + */ + public function setKeyResolver(KeyResolver $keyResolver): self + { + $this->keyResolver = $keyResolver; + + return $this; + } + /** * @return Paginator */ diff --git a/src/OrionServiceProvider.php b/src/OrionServiceProvider.php index d97fb202..06ff705f 100644 --- a/src/OrionServiceProvider.php +++ b/src/OrionServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Support\ServiceProvider; use Orion\Commands\BuildSpecsCommand; use Orion\Contracts\ComponentsResolver; +use Orion\Contracts\KeyResolver; use Orion\Contracts\Paginator; use Orion\Contracts\ParamsValidator; use Orion\Contracts\QueryBuilder; @@ -29,6 +30,7 @@ public function register() $this->app->bind(Paginator::class, Drivers\Standard\Paginator::class); $this->app->bind(SearchBuilder::class, Drivers\Standard\SearchBuilder::class); $this->app->bind(ComponentsResolver::class, Drivers\Standard\ComponentsResolver::class); + $this->app->bind(KeyResolver::class, Drivers\Standard\KeyResolver::class); $this->app->singleton(ResourcesCacheStore::class); } @@ -42,7 +44,6 @@ public function boot() { app('router')->pushMiddlewareToGroup('api', EnforceExpectsJson::class); - $this->publishes( [ __DIR__ . '/../config/orion.php' => config_path('orion.php'), diff --git a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardDeleteOperationsTest.php b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardDeleteOperationsTest.php index e1f7e421..3c05382c 100644 --- a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardDeleteOperationsTest.php +++ b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardDeleteOperationsTest.php @@ -2,10 +2,13 @@ namespace Orion\Tests\Feature\Relations\BelongsTo; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Mockery; use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Category; use Orion\Tests\Fixtures\App\Models\Post; @@ -158,4 +161,37 @@ public function deleting_a_single_relation_resource_and_getting_included_relatio $this->assertResourceDeleted($response, $user, ['posts' => [$post->toArray()]]); } + + /** @test */ + public function deleting_a_single_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $category = factory(Category::class)->create(); + $post = factory(Post::class)->create(['category_id' => $category->id]); + + Gate::policy(Category::class, GreenPolicy::class); + + $response = $this->delete("/api/v1/posts/{$post->id}/category"); + + $this->assertResourceTrashed($response, $category); + } + + /** @test */ + public function deleting_a_single_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $category = factory(Category::class)->create(); + $post = factory(Post::class)->create(['category_id' => $category->id]); + + Gate::policy(Category::class, GreenPolicy::class); + + $response = $this->delete("/api/v1/posts/{$post->id}/category"); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardRestoreOperationsTest.php b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardRestoreOperationsTest.php index cd057860..99ead972 100644 --- a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardRestoreOperationsTest.php +++ b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardRestoreOperationsTest.php @@ -2,10 +2,11 @@ namespace Orion\Tests\Feature\Relations\BelongsTo; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Category; use Orion\Tests\Fixtures\App\Models\Post; @@ -94,4 +95,37 @@ public function restoring_a_single_relation_resource_and_getting_included_relati $this->assertResourceRestored($response, $trashedCategory, ['posts' => $trashedCategory->posts->toArray()]); } + + /** @test */ + public function restoring_a_single_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $trashedCategory = factory(Category::class)->state('trashed')->create(); + $post = factory(Post::class)->create(['category_id' => $trashedCategory->id]); + + Gate::policy(Category::class, GreenPolicy::class); + + $response = $this->post("/api/v1/posts/{$post->id}/category/{$trashedCategory->id}/restore"); + + $this->assertResourceRestored($response, $trashedCategory); + } + + /** @test */ + public function restoring_a_single_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $trashedCategory = factory(Category::class)->state('trashed')->create(); + $post = factory(Post::class)->create(['category_id' => $trashedCategory->id]); + + Gate::policy(Category::class, GreenPolicy::class); + + $response = $this->post("/api/v1/posts/{$post->id}/category/{$trashedCategory->id}/restore"); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardShowOperationsTest.php b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardShowOperationsTest.php index ea673c61..5d7dc8ce 100644 --- a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardShowOperationsTest.php +++ b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardShowOperationsTest.php @@ -2,8 +2,11 @@ namespace Orion\Tests\Feature\Relations\BelongsTo; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Orion\Tests\Feature\TestCase; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Category; use Orion\Tests\Fixtures\App\Models\Post; @@ -65,7 +68,6 @@ public function getting_a_single_trashed_relation_resource_when_with_trashed_que $this->assertResourceShown($response, $trashedCategory); } - /** @test */ public function getting_a_single_transformed_relation_resource(): void { @@ -110,4 +112,37 @@ public function getting_a_single_relation_resource_with_aggregate(): void $this->assertResourceShown($response, $user->fresh()->loadCount('posts')->toArray()); } + + /** @test */ + public function getting_a_single_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + + Gate::policy(User::class, GreenPolicy::class); + + $response = $this->get("/api/v1/posts/{$post->id}/user"); + + $this->assertResourceShown($response, $user); + } + + /** @test */ + public function getting_a_single_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + + Gate::policy(User::class, GreenPolicy::class); + + $response = $this->get("/api/v1/posts/{$post->id}/user"); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardUpdateOperationsTest.php b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardUpdateOperationsTest.php index 46472018..5dea3420 100644 --- a/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardUpdateOperationsTest.php +++ b/tests/Feature/Relations/BelongsTo/BelongsToRelationStandardUpdateOperationsTest.php @@ -2,8 +2,11 @@ namespace Orion\Tests\Feature\Relations\BelongsTo; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Orion\Tests\Feature\TestCase; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Requests\UserRequest; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Post; @@ -92,7 +95,7 @@ public function transforming_a_single_updated_relation_resource(): void $post = factory(Post::class)->create(['user_id' => $user->id]); $payload = ['name' => 'test user updated']; - $this->useResource(SampleResource::class); + $this->useResource(SampleResource::class); Gate::policy(User::class, GreenPolicy::class); @@ -126,4 +129,44 @@ public function updating_a_single_resource_and_getting_included_relation(): void ['posts' => $user->fresh('posts')->posts->toArray()] ); } + + /** @test */ + public function updating_a_single_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + $payload = ['name' => 'test user updated']; + + Gate::policy(User::class, GreenPolicy::class); + + $response = $this->patch("/api/v1/posts/{$post->id}/user", $payload); + + $this->assertResourceUpdated( + $response, + User::class, + $user->toArray(), + $payload + ); + } + + /** @test */ + public function updating_a_single_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + $payload = ['name' => 'test user updated']; + + Gate::policy(User::class, GreenPolicy::class); + + $response = $this->patch("/api/v1/posts/{$post->id}/user", $payload); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationManyToManyOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationManyToManyOperationsTest.php index 7b046233..4efc2e99 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationManyToManyOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationManyToManyOperationsTest.php @@ -4,8 +4,11 @@ namespace Orion\Tests\Feature\Relations\BelongsToMany; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Orion\Tests\Feature\TestCase; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Models\Role; use Orion\Tests\Fixtures\App\Models\User; use Orion\Tests\Fixtures\App\Policies\GreenPolicy; @@ -46,7 +49,6 @@ public function attaching_relation_resources_when_authorized_only_on_parent(): v self::assertEquals(0, $user->roles()->count()); - $response = $this->post( "/api/users/{$user->id}/roles/attach", [ @@ -754,4 +756,307 @@ public function updating_pivot_of_relation_resource_casted_to_json_pivot_field() ['references' => ['key' => 'value']] ); } + + /** @test */ + public function attaching_relation_resources_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + /** @var User $user */ + $user = factory(User::class)->create(); + $roleA = factory(Role::class)->create(); + $roleB = factory(Role::class)->create(); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + self::assertEquals(0, $user->roles()->count()); + + $response = $this->post( + "/api/v1/users/{$user->id}/roles/attach", + [ + 'resources' => [$roleA->id, $roleB->id], + ] + ); + + $this->assertResourcesAttached( + $response, + 'roles', + $user, + collect([$roleA, $roleB]) + ); + } + + /** @test */ + public function attaching_relation_resources_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + /** @var User $user */ + $user = factory(User::class)->create(); + $roleA = factory(Role::class)->create(); + $roleB = factory(Role::class)->create(); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + self::assertEquals(0, $user->roles()->count()); + + $response = $this->post( + "/api/v1/users/{$user->id}/roles/attach", + [ + 'resources' => [$roleA->id, $roleB->id], + ] + ); + + $response->assertNotFound(); + } + + /** @test */ + public function detaching_relation_resources_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + /** @var User $user */ + $user = factory(User::class)->create(); + $roles = factory(Role::class)->times(5)->make(); + $user->roles()->saveMany($roles); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + $roles = $user->roles()->get(); + + self::assertEquals(5, $roles->count()); + + $response = $this->delete( + "/api/v1/users/{$user->id}/roles/detach", + [ + 'resources' => $roles->pluck('id')->toArray(), + ] + ); + + $this->assertResourcesDetached($response, 'roles', $user, $roles); + } + + /** @test */ + public function detaching_relation_resources_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + /** @var User $user */ + $user = factory(User::class)->create(); + $roles = factory(Role::class)->times(5)->make(); + $user->roles()->saveMany($roles); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + $roles = $user->roles()->get(); + + self::assertEquals(5, $roles->count()); + + $response = $this->delete( + "/api/v1/users/{$user->id}/roles/detach", + [ + 'resources' => $roles->pluck('id')->toArray(), + ] + ); + + $response->assertNotFound(); + } + + /** @test */ + public function syncing_relation_resources_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + /** @var User $user */ + $user = factory(User::class)->create(); + $roleToAttach = factory(Role::class)->create(); + $roleToDetach = factory(Role::class)->create(); + $roleToUpdate = factory(Role::class)->create(); + $user->roles()->attach($roleToDetach->id); + $user->roles()->attach($roleToUpdate->id, ['custom_name' => 'test original']); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + self::assertEquals(2, $user->roles()->count()); + + $response = $this->patch( + "/api/v1/users/{$user->id}/roles/sync", + [ + 'resources' => [ + $roleToAttach->id, + $roleToUpdate->id => ['custom_name' => 'test updated'], + ], + ] + ); + + $this->assertResourcesSynced( + $response, + 'roles', + $user, + $this->buildSyncMap([$roleToAttach], [$roleToDetach], [$roleToUpdate]), + [$roleToUpdate->id => ['custom_name' => 'test updated']] + ); + } + + /** @test */ + public function syncing_relation_resources_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + /** @var User $user */ + $user = factory(User::class)->create(); + $roleToAttach = factory(Role::class)->create(); + $roleToDetach = factory(Role::class)->create(); + $roleToUpdate = factory(Role::class)->create(); + $user->roles()->attach($roleToDetach->id); + $user->roles()->attach($roleToUpdate->id, ['custom_name' => 'test original']); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + self::assertEquals(2, $user->roles()->count()); + + $response = $this->patch( + "/api/v1/users/{$user->id}/roles/sync", + [ + 'resources' => [ + $roleToAttach->id, + $roleToUpdate->id => ['custom_name' => 'test updated'], + ], + ] + ); + + $response->assertNotFound(); + } + + /** @test */ + public function toggling_relation_resources_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + /** @var User $user */ + $user = factory(User::class)->create(); + $roleToAttach = factory(Role::class)->create(); + $roleToDetach = factory(Role::class)->create(); + $user->roles()->attach($roleToDetach->id); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + self::assertEquals(1, $user->roles()->count()); + + $response = $this->patch( + "/api/v1/users/{$user->id}/roles/toggle", + [ + 'resources' => [$roleToAttach->id, $roleToDetach->id], + ] + ); + + $this->assertResourcesToggled( + $response, + 'roles', + $user, + $this->buildSyncMap([$roleToAttach], [$roleToDetach]) + ); + } + + /** @test */ + public function toggling_relation_resources_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + /** @var User $user */ + $user = factory(User::class)->create(); + $roleToAttach = factory(Role::class)->create(); + $roleToDetach = factory(Role::class)->create(); + $user->roles()->attach($roleToDetach->id); + + Gate::policy(User::class, GreenPolicy::class); + Gate::policy(Role::class, GreenPolicy::class); + + self::assertEquals(1, $user->roles()->count()); + + $response = $this->patch( + "/api/v1/users/{$user->id}/roles/toggle", + [ + 'resources' => [$roleToAttach->id, $roleToDetach->id], + ] + ); + + $response->assertNotFound(); + } + + /** @test */ + public function updating_pivot_of_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + /** @var User $user */ + $user = factory(User::class)->create(); + $role = factory(Role::class)->create(); + $user->roles()->save($role); + + Gate::policy(Role::class, GreenPolicy::class); + + $response = $this->patch( + "/api/v1/users/{$user->id}/roles/{$role->id}/pivot", + [ + 'pivot' => [ + 'custom_name' => 'test value', + ], + ] + ); + + $this->assertResourcePivotUpdated( + $response, + 'roles', + $user, + $role, + ['custom_name' => 'test value'] + ); + } + + /** @test */ + public function updating_pivot_of_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + /** @var User $user */ + $user = factory(User::class)->create(); + $role = factory(Role::class)->create(); + $user->roles()->save($role); + + Gate::policy(Role::class, GreenPolicy::class); + + $response = $this->patch( + "/api/v1/users/{$user->id}/roles/{$role->id}/pivot", + [ + 'pivot' => [ + 'custom_name' => 'test value', + ], + ] + ); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardDeleteOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardDeleteOperationsTest.php index 13012507..afc2c1c4 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardDeleteOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardDeleteOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\BelongsToMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Notification; diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexOperationsTest.php index 1fa2a32a..b85264af 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\BelongsToMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleCollectionResource; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardRestoreOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardRestoreOperationsTest.php index 9541bf98..d6addaf6 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardRestoreOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardRestoreOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\BelongsToMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Notification; diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardShowOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardShowOperationsTest.php index e26956cd..4cc9838d 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardShowOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardShowOperationsTest.php @@ -95,7 +95,6 @@ public function getting_a_single_trashed_relation_resource_when_with_trashed_que $this->assertResourceShown($response, $user->notifications()->withTrashed()->first()->toArray()); } - /** @test */ public function getting_a_single_transformed_relation_resource(): void { diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardStoreOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardStoreOperationsTest.php index 203b3c85..8bd8b324 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardStoreOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardStoreOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\BelongsToMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Requests\RoleRequest; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; diff --git a/tests/Feature/Relations/HasMany/HasManyRelationOneToManyOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationOneToManyOperationsTest.php index 45fd3e59..701594a5 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationOneToManyOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationOneToManyOperationsTest.php @@ -2,10 +2,11 @@ namespace Orion\Tests\Feature\Relations\HasMany; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Company; use Orion\Tests\Fixtures\App\Models\Team; @@ -204,4 +205,89 @@ public function transforming_a_dissociated_relation_resource_when_authorized(): ['test-field-from-resource' => 'test-value'] ); } + + /** @test */ + public function associating_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $company = factory(Company::class)->create(); + $team = factory(Team::class)->create(); + + Gate::policy(Company::class, GreenPolicy::class); + Gate::policy(Team::class, GreenPolicy::class); + + $response = $this->post( + "/api/v1/companies/{$company->id}/teams/associate", + [ + 'related_key' => $team->id, + ] + ); + + $this->assertResourceAssociated($response, $company, $team, 'company'); + } + + /** @test */ + public function associating_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $company = factory(Company::class)->create(); + $team = factory(Team::class)->create(); + + Gate::policy(Company::class, GreenPolicy::class); + Gate::policy(Team::class, GreenPolicy::class); + + $response = $this->post( + "/api/v1/companies/{$company->id}/teams/associate", + [ + 'related_key' => $team->id, + ] + ); + + $response->assertNotFound(); + } + + /** @test */ + public function disassociating_relation_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $company = factory(Company::class)->create(); + $team = factory(Team::class)->create(['company_id' => $company->id]); + + Gate::policy(Company::class, GreenPolicy::class); + Gate::policy(Team::class, GreenPolicy::class); + + $response = $this->delete("/api/v1/companies/{$company->id}/teams/{$team->id}/dissociate"); + + $this->assertResourceDissociated($response, $team, 'company'); + } + + /** @test */ + public function disassociating_relation_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $company = factory(Company::class)->create(); + $team = factory(Team::class)->create(); + + Gate::policy(Company::class, GreenPolicy::class); + Gate::policy(Team::class, GreenPolicy::class); + + $response = $this->post( + "/api/v1/companies/{$company->id}/teams/associate", + [ + 'related_key' => $team->id, + ] + ); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/Relations/HasMany/HasManyRelationStandardDeleteOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationStandardDeleteOperationsTest.php index 22411ee9..5283e657 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationStandardDeleteOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationStandardDeleteOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\AccessKey; diff --git a/tests/Feature/Relations/HasMany/HasManyRelationStandardIndexOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationStandardIndexOperationsTest.php index d8297a03..3e9d7e0a 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationStandardIndexOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationStandardIndexOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleCollectionResource; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; diff --git a/tests/Feature/Relations/HasMany/HasManyRelationStandardRestoreOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationStandardRestoreOperationsTest.php index 4924f9dc..032849e1 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationStandardRestoreOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationStandardRestoreOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\AccessKey; diff --git a/tests/Feature/Relations/HasMany/HasManyRelationStandardShowOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationStandardShowOperationsTest.php index b26e4ba5..492737f3 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationStandardShowOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationStandardShowOperationsTest.php @@ -81,7 +81,6 @@ public function getting_a_single_trashed_relation_resource_when_with_trashed_que $this->assertResourceShown($response, $trashedPost); } - /** @test */ public function getting_a_single_transformed_relation_resource(): void { diff --git a/tests/Feature/Relations/HasMany/HasManyRelationStandardStoreOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationStandardStoreOperationsTest.php index 50952f0e..93fc0eb7 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationStandardStoreOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationStandardStoreOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Requests\TeamRequest; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; diff --git a/tests/Feature/Relations/HasMany/HasManyRelationStandardUpdateOperationsTest.php b/tests/Feature/Relations/HasMany/HasManyRelationStandardUpdateOperationsTest.php index 70e4d46e..bbec5d49 100644 --- a/tests/Feature/Relations/HasMany/HasManyRelationStandardUpdateOperationsTest.php +++ b/tests/Feature/Relations/HasMany/HasManyRelationStandardUpdateOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasMany; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Requests\TeamRequest; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; diff --git a/tests/Feature/Relations/HasOne/HasOneRelationStandardDeleteOperationsTest.php b/tests/Feature/Relations/HasOne/HasOneRelationStandardDeleteOperationsTest.php index 7aae4407..e2e6298c 100644 --- a/tests/Feature/Relations/HasOne/HasOneRelationStandardDeleteOperationsTest.php +++ b/tests/Feature/Relations/HasOne/HasOneRelationStandardDeleteOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasOne; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Post; diff --git a/tests/Feature/Relations/HasOne/HasOneRelationStandardRestoreOperationsTest.php b/tests/Feature/Relations/HasOne/HasOneRelationStandardRestoreOperationsTest.php index 20822a84..f27f1523 100644 --- a/tests/Feature/Relations/HasOne/HasOneRelationStandardRestoreOperationsTest.php +++ b/tests/Feature/Relations/HasOne/HasOneRelationStandardRestoreOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasOne; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Post; diff --git a/tests/Feature/Relations/HasOne/HasOneRelationStandardShowOperationsTest.php b/tests/Feature/Relations/HasOne/HasOneRelationStandardShowOperationsTest.php index d8fa1618..3cd9f407 100644 --- a/tests/Feature/Relations/HasOne/HasOneRelationStandardShowOperationsTest.php +++ b/tests/Feature/Relations/HasOne/HasOneRelationStandardShowOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasOne; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Post; @@ -67,7 +65,6 @@ public function getting_a_single_trashed_relation_resource_when_with_trashed_que $this->assertResourceShown($response, $trashedPostMeta); } - /** @test */ public function getting_a_single_transformed_relation_resource(): void { diff --git a/tests/Feature/Relations/HasOne/HasOneRelationStandardUpdateOperationsTest.php b/tests/Feature/Relations/HasOne/HasOneRelationStandardUpdateOperationsTest.php index 8baf5939..29be47d9 100644 --- a/tests/Feature/Relations/HasOne/HasOneRelationStandardUpdateOperationsTest.php +++ b/tests/Feature/Relations/HasOne/HasOneRelationStandardUpdateOperationsTest.php @@ -3,8 +3,6 @@ namespace Orion\Tests\Feature\Relations\HasOne; use Illuminate\Support\Facades\Gate; -use Mockery; -use Orion\Contracts\ComponentsResolver; use Orion\Tests\Feature\TestCase; use Orion\Tests\Fixtures\App\Http\Requests\PostMetaRequest; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; diff --git a/tests/Feature/StandardDeleteOperationsTest.php b/tests/Feature/StandardDeleteOperationsTest.php index 34becb94..c6d695b2 100644 --- a/tests/Feature/StandardDeleteOperationsTest.php +++ b/tests/Feature/StandardDeleteOperationsTest.php @@ -2,9 +2,12 @@ namespace Orion\Tests\Feature; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Mockery; use Orion\Contracts\ComponentsResolver; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\AccessKey; use Orion\Tests\Fixtures\App\Models\Post; @@ -186,4 +189,37 @@ public function deleting_a_single_resource_and_getting_included_relation(): void $this->assertResourceDeleted($response, $post, ['user' => $user->toArray()]); } + + /** @test */ + public function deleting_a_single_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->delete("/api/v1/posts/$post->id"); + + $this->assertResourceTrashed($response, $post); + } + + /** @test */ + public function deleting_a_single_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->delete("/api/v1/posts/$post->id"); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/StandardIncludeOperationsTest.php b/tests/Feature/StandardIncludeOperationsTest.php index c77d8d5b..350c11d3 100644 --- a/tests/Feature/StandardIncludeOperationsTest.php +++ b/tests/Feature/StandardIncludeOperationsTest.php @@ -106,7 +106,7 @@ public function ensuring_root_level_filters_are_not_applied_on_includes(): void $this->assertResourcesPaginated( $response, - $this->makePaginator([$user->load(['posts'])->toArray(),], 'users/search'), + $this->makePaginator([$user->load(['posts'])->toArray()], 'users/search'), [], false ); diff --git a/tests/Feature/StandardIndexFilteringOperationsTest.php b/tests/Feature/StandardIndexFilteringOperationsTest.php index c2707e6b..93bf7385 100644 --- a/tests/Feature/StandardIndexFilteringOperationsTest.php +++ b/tests/Feature/StandardIndexFilteringOperationsTest.php @@ -681,7 +681,7 @@ public function getting_a_list_of_resources_filtered_by_identically_named_relati { $user = factory(User::class)->create(['name' => 'John Doe']); $matchingPost = factory(Post::class) - ->create([ 'user_id' => $user->id, ])->fresh(); + ->create(['user_id' => $user->id])->fresh(); factory(PostMeta::class)->create(['post_id' => $matchingPost->id, 'name' => 'test']); factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh(); @@ -708,10 +708,9 @@ public function getting_a_list_of_resources_filtered_by_identically_named_relati public function getting_a_list_of_resources_filtered_by_identically_named_fields_on_different_nesting_level(): void { $matchingPost = factory(Post::class) - ->create(['title' => 'test' ])->fresh(); + ->create(['title' => 'test'])->fresh(); factory(PostMeta::class)->create(['post_id' => $matchingPost->id, 'title' => 'test']); - factory(Post::class)->create(['publish_at' => Carbon::now()])->fresh(); Gate::policy(Post::class, GreenPolicy::class); diff --git a/tests/Feature/StandardIndexOperationsTest.php b/tests/Feature/StandardIndexOperationsTest.php index 9c6f194e..74caf148 100644 --- a/tests/Feature/StandardIndexOperationsTest.php +++ b/tests/Feature/StandardIndexOperationsTest.php @@ -5,7 +5,6 @@ use Illuminate\Support\Facades\Gate; use Mockery; use Orion\Contracts\ComponentsResolver; -use Orion\Exceptions\MaxPaginationLimitExceededException; use Orion\Tests\Fixtures\App\Http\Resources\SampleCollectionResource; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\Post; diff --git a/tests/Feature/StandardRestoreOperationsTest.php b/tests/Feature/StandardRestoreOperationsTest.php index 42003b69..cf40a455 100644 --- a/tests/Feature/StandardRestoreOperationsTest.php +++ b/tests/Feature/StandardRestoreOperationsTest.php @@ -2,9 +2,12 @@ namespace Orion\Tests\Feature; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Mockery; use Orion\Contracts\ComponentsResolver; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\AccessKey; use Orion\Tests\Fixtures\App\Models\Post; @@ -109,4 +112,35 @@ public function restoring_a_single_resource_and_getting_included_relation(): voi $this->assertResourceRestored($response, $trashedPost, ['user' => $user->toArray()]); } + + /** @test */ + public function restoring_a_single_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $trashedPost = factory(Post::class)->state('trashed')->create(['user_id' => factory(User::class)->create()->id]); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->post("/api/v1/posts/{$trashedPost->id}/restore"); + + $this->assertResourceRestored($response, $trashedPost); + } + + /** @test */ + public function restoring_a_single_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $trashedPost = factory(Post::class)->state('trashed')->create(['user_id' => factory(User::class)->create()->id]); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->post("/api/v1/posts/{$trashedPost->id}/restore"); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/StandardShowOperationsTest.php b/tests/Feature/StandardShowOperationsTest.php index 46c8fc11..03795eaa 100644 --- a/tests/Feature/StandardShowOperationsTest.php +++ b/tests/Feature/StandardShowOperationsTest.php @@ -2,9 +2,12 @@ namespace Orion\Tests\Feature; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Mockery; use Orion\Contracts\ComponentsResolver; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\AccessKey; use Orion\Tests\Fixtures\App\Models\Comment; @@ -166,4 +169,37 @@ public function getting_a_single_resource_with_included_relation_whitelisted_by_ $this->assertResourceShown($response, $team->fresh('company')->toArray()); } + + /** @test */ + public function getting_a_single_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->get("/api/v1/posts/$post->id"); + + $this->assertResourceShown($response, $post); + } + + /** @test */ + public function getting_a_single_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->get("/api/v1/posts/$post->id"); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/StandardUpdateOperationsTest.php b/tests/Feature/StandardUpdateOperationsTest.php index b8c23928..4264ee6f 100644 --- a/tests/Feature/StandardUpdateOperationsTest.php +++ b/tests/Feature/StandardUpdateOperationsTest.php @@ -2,9 +2,12 @@ namespace Orion\Tests\Feature; +use Illuminate\Database\QueryException; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Mockery; use Orion\Contracts\ComponentsResolver; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Http\Requests\PostRequest; use Orion\Tests\Fixtures\App\Http\Resources\SampleResource; use Orion\Tests\Fixtures\App\Models\AccessKey; @@ -158,4 +161,44 @@ public function updating_a_single_resource_and_getting_included_relation(): void ['user' => $user->fresh()->toArray()] ); } + + /** @test */ + public function updating_a_single_resource_with_multiple_route_parameters(): void + { + $this->useKeyResolver(TwoRouteParameterKeyResolver::class); + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + $payload = ['title' => 'test post title updated']; + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->patch("/api/v1/posts/$post->id", $payload); + + $this->assertResourceUpdated( + $response, + Post::class, + $post->toArray(), + $payload + ); + } + + /** @test */ + public function updating_a_single_resource_with_multiple_route_parameters_fails_with_default_key_resolver(): void + { + if (DB::connection()->getDriverName() === 'pgsql') { + $this->withoutExceptionHandling(); + $this->expectException(QueryException::class); + } + + $user = factory(User::class)->create(); + $post = factory(Post::class)->create(['user_id' => $user->id]); + $payload = ['title' => 'test post title updated']; + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->patch("/api/v1/posts/$post->id", $payload); + + $response->assertNotFound(); + } } diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index a2a8cbcc..204cc818 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -4,15 +4,17 @@ use Mockery; use Orion\Contracts\ComponentsResolver; +use Orion\Contracts\KeyResolver; use Orion\Testing\InteractsWithAuthorization; use Orion\Testing\InteractsWithJsonFields; use Orion\Testing\InteractsWithResources; +use Orion\Tests\Fixtures\App\Drivers\TwoRouteParameterKeyResolver; use Orion\Tests\Fixtures\App\Models\User; use Orion\Tests\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { - use InteractsWithResources, InteractsWithJsonFields, InteractsWithAuthorization; + use InteractsWithAuthorization, InteractsWithJsonFields, InteractsWithResources; protected function setUp(): void { @@ -86,4 +88,14 @@ function () use ($collectionResourceClass) { return $this; } + + protected function useKeyResolver(string $keyResolverClass): self + { + app()->bind( + KeyResolver::class, + TwoRouteParameterKeyResolver::class + ); + + return $this; + } } diff --git a/tests/Fixtures/app/Drivers/TwoRouteParameterKeyResolver.php b/tests/Fixtures/app/Drivers/TwoRouteParameterKeyResolver.php new file mode 100644 index 00000000..be03ed31 --- /dev/null +++ b/tests/Fixtures/app/Drivers/TwoRouteParameterKeyResolver.php @@ -0,0 +1,24 @@ +app->bind(Paginator::class, \Orion\Drivers\Standard\Paginator::class); $this->app->bind(SearchBuilder::class, \Orion\Drivers\Standard\SearchBuilder::class); $this->app->bind(ComponentsResolver::class, \Orion\Drivers\Standard\ComponentsResolver::class); + $this->app->bind(KeyResolver::class, \Orion\Drivers\Standard\KeyResolver::class); $this->app->singleton(ResourcesCacheStore::class); } diff --git a/tests/Fixtures/routes/api.php b/tests/Fixtures/routes/api.php index 72dd82be..d22604d1 100644 --- a/tests/Fixtures/routes/api.php +++ b/tests/Fixtures/routes/api.php @@ -33,4 +33,15 @@ Orion::hasManyResource('access_keys', 'scopes', AccessKeyAccessKeyScopesController::class)->withSoftDeletes(); Orion::belongsToManyResource('users', 'roles', UserRolesController::class); Orion::belongsToManyResource('users', 'notifications', UserNotificationsController::class)->withSoftDeletes(); + + Route::group(['prefix' => '{apiVersion}'], function () { + Orion::resource('posts', PostsController::class)->withSoftDeletes(); + + Orion::belongsToResource('posts', 'user', PostUserController::class); + Orion::belongsToResource('posts', 'category', PostCategoryController::class)->withSoftDeletes(); + + Orion::hasManyResource('companies', 'teams', CompanyTeamsController::class); + + Orion::belongsToManyResource('users', 'roles', UserRolesController::class); + }); }); diff --git a/tests/Unit/Http/Controllers/BaseControllerTest.php b/tests/Unit/Http/Controllers/BaseControllerTest.php index b023988a..71d23943 100644 --- a/tests/Unit/Http/Controllers/BaseControllerTest.php +++ b/tests/Unit/Http/Controllers/BaseControllerTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\App; use Mockery; use Orion\Drivers\Standard\ComponentsResolver; +use Orion\Drivers\Standard\KeyResolver; use Orion\Drivers\Standard\Paginator; use Orion\Drivers\Standard\ParamsValidator; use Orion\Drivers\Standard\QueryBuilder; @@ -25,7 +26,6 @@ class BaseControllerTest extends TestCase { - /** @test */ public function binding_exception_is_thrown_if_model_is_not_set() { @@ -44,6 +44,7 @@ public function dependencies_are_resolved_correctly() $fakePaginator = new Paginator(15, null); $fakeSearchBuilder = new SearchBuilder([]); $fakeQueryBuilder = new QueryBuilder(Post::class, $fakeParamsValidator, $fakeRelationsResolver, $fakeSearchBuilder); + $fakeKeyResolver = new KeyResolver(); App::shouldReceive('makeWith')->with( \Orion\Contracts\ComponentsResolver::class, @@ -97,6 +98,10 @@ public function dependencies_are_resolved_correctly() ] )->once()->andReturn($fakeQueryBuilder); + App::shouldReceive('make')->with( + \Orion\Contracts\KeyResolver::class, + )->once()->andReturn($fakeKeyResolver); + $stub = new BaseControllerStubWithWhitelistedFieldsAndRelations(); $this->assertEquals($fakeComponentsResolver, $stub->getComponentsResolver()); $this->assertEquals($fakeParamsValidator, $stub->getParamsValidator()); diff --git a/tests/Unit/Http/Controllers/RelationControllerTest.php b/tests/Unit/Http/Controllers/RelationControllerTest.php index 8cfabf15..fd7dda68 100644 --- a/tests/Unit/Http/Controllers/RelationControllerTest.php +++ b/tests/Unit/Http/Controllers/RelationControllerTest.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\App; use Orion\Drivers\Standard\ComponentsResolver; +use Orion\Drivers\Standard\KeyResolver; use Orion\Drivers\Standard\Paginator; use Orion\Drivers\Standard\ParamsValidator; use Orion\Drivers\Standard\QueryBuilder; @@ -39,6 +40,7 @@ public function dependencies_are_resolved_correctly(): void $fakeSearchBuilder = new SearchBuilder([]); $fakeQueryBuilder = new QueryBuilder(Post::class, $fakeParamsValidator, $fakeRelationsResolver, $fakeSearchBuilder); $fakeRelationQueryBuilder = new QueryBuilder(User::class, $fakeParamsValidator, $fakeRelationsResolver, $fakeSearchBuilder); + $fakeKeyResolver = new KeyResolver(); App::shouldReceive('makeWith')->with( \Orion\Contracts\ComponentsResolver::class, @@ -109,6 +111,10 @@ public function dependencies_are_resolved_correctly(): void ] )->once()->andReturn($fakeRelationQueryBuilder); + App::shouldReceive('make')->with( + \Orion\Contracts\KeyResolver::class, + )->once()->andReturn($fakeKeyResolver); + $stub = new RelationControllerStub(); $this->assertEquals($fakeComponentsResolver, $stub->getComponentsResolver()); $this->assertEquals($fakeParentComponentsResolver, $stub->getParentComponentsResolver());