From 90af41a347b0950ee87252a6feca11b0de49ca9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Marques?= Date: Sat, 25 Jan 2025 00:30:37 +0000 Subject: [PATCH] feat(Project): Introduced a policy to control the access --- .../Controllers/Api/ActivityController.php | 1 - app/Http/Requests/CreateActivityRequest.php | 4 +- app/Policies/ProjectPolicy.php | 22 ++++++ app/Providers/AppServiceProvider.php | 12 +++ database/factories/UserFactory.php | 7 +- routes/web.php | 2 +- .../Http/Api/ActivityControllerTest.php | 74 ++++++++++++------- tests/Unit/Policies/ProjectPolicyTest.php | 23 ++++++ 8 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 app/Policies/ProjectPolicy.php create mode 100644 tests/Unit/Policies/ProjectPolicyTest.php diff --git a/app/Http/Controllers/Api/ActivityController.php b/app/Http/Controllers/Api/ActivityController.php index ba97834..f9ba821 100644 --- a/app/Http/Controllers/Api/ActivityController.php +++ b/app/Http/Controllers/Api/ActivityController.php @@ -7,7 +7,6 @@ use App\Actions\CreateActivityAction; use App\Http\Requests\CreateActivityRequest; use App\Models\Project; -use Illuminate\Http\Request; use Illuminate\Http\Response; final readonly class ActivityController diff --git a/app/Http/Requests/CreateActivityRequest.php b/app/Http/Requests/CreateActivityRequest.php index 4255db7..67d6284 100644 --- a/app/Http/Requests/CreateActivityRequest.php +++ b/app/Http/Requests/CreateActivityRequest.php @@ -1,12 +1,14 @@ id === $project->user_id; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 581aa82..5ff2765 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,10 +4,13 @@ namespace App\Providers; +use App\Models\Project; +use App\Policies\ProjectPolicy; use Carbon\CarbonImmutable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\Vite; use Illuminate\Support\ServiceProvider; @@ -32,6 +35,7 @@ public function boot(): void $this->configureDates(); $this->configureUrls(); $this->configureVite(); + $this->configurePolicies(); } /** @@ -77,4 +81,12 @@ private function configureVite(): void { Vite::useAggressivePrefetching(); } + + /** + * Configure the application's policies. + */ + private function configurePolicies(): void + { + Gate::policy(Project::class, ProjectPolicy::class); + } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index dba6cfa..e511241 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -4,12 +4,17 @@ namespace Database\Factories; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @method Collection|User create($attributes = [], ?Model $parent = null) + * + * @extends Factory */ final class UserFactory extends Factory { diff --git a/routes/web.php b/routes/web.php index a4e9640..71d48f5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -10,7 +10,7 @@ }); Route::prefix('api')->group(function () { - Route::prefix('{project}')->group(function () { + Route::prefix('{project}')->middleware('can:view,project')->group(function () { Route::post('activities', [ActivityController::class, 'store'])->name('api.activities.store'); }); }); diff --git a/tests/Feature/Http/Api/ActivityControllerTest.php b/tests/Feature/Http/Api/ActivityControllerTest.php index ca3c5d6..bbd6ee3 100644 --- a/tests/Feature/Http/Api/ActivityControllerTest.php +++ b/tests/Feature/Http/Api/ActivityControllerTest.php @@ -5,6 +5,7 @@ use App\Enums\EventType; use App\Jobs\IngestActivity; use App\Models\Project; +use App\Models\User; use Illuminate\Support\Facades\Queue; beforeEach()->only(); @@ -12,16 +13,18 @@ it('can create an activity', function () { // Arrange... Queue::fake([IngestActivity::class]); - $project = Project::factory()->create()->fresh(); + $authUser = User::factory()->create(); + $project = Project::factory()->for($authUser)->create()->fresh(); $events = [ EventType::view('/about'), ]; // Act... - $response = $this->postJson(route('api.activities.store', $project), [ - 'events' => $events, - ]); + $response = $this->actingAs($authUser) + ->postJson(route('api.activities.store', $project), [ + 'events' => $events, + ]); // Assert... $response->assertStatus(201); @@ -32,15 +35,34 @@ Queue::assertPushed(IngestActivity::class, 1); }); +it('cannot create an activity for a project that does not belong to the user', function () { + Queue::fake([IngestActivity::class]); + $project = Project::factory()->create(); + + $this->actingAs(User::factory()->create()) + ->postJson(route('api.activities.store', $project), [ + 'events' => [ + EventType::view('/about'), + ], + ]) + ->assertStatus(403); + + $this->assertDatabaseCount('activities', 0); + + Queue::assertNotPushed(IngestActivity::class); +}); + it('does not handle empty events', function () { // Arrange... Queue::fake([IngestActivity::class]); - $project = Project::factory()->create()->fresh(); + $authUser = User::factory()->create(); + $project = Project::factory()->for($authUser)->create()->fresh(); // Act... - $response = $this->postJson(route('api.activities.store', $project), [ - 'events' => [], - ]); + $response = $this->actingAs($authUser) + ->postJson(route('api.activities.store', $project), [ + 'events' => [], + ]); // Assert... $response->assertStatus(422)->assertJsonValidationErrors([ @@ -56,27 +78,29 @@ it('does not handle corrupted events', function () { // Arrange... Queue::fake([IngestActivity::class]); - $project = Project::factory()->create()->fresh(); + $authUser = User::factory()->create(); + $project = Project::factory()->for($authUser)->create()->fresh(); // Act... - $response = $this->postJson(route('api.activities.store', $project), [ - 'events' => [ - 1, - 'string', - [ + $response = $this->actingAs($authUser) + ->postJson(route('api.activities.store', $project), [ + 'events' => [ 1, - ], - [ - 'type' => 'view', - ], - [ - 'type' => 'view', - 'payload' => [ - // + 'string', + [ + 1, ], - ] - ], - ]); + [ + 'type' => 'view', + ], + [ + 'type' => 'view', + 'payload' => [ + // + ], + ], + ], + ]); // Assert... $response->assertStatus(422)->assertJsonValidationErrors([ diff --git a/tests/Unit/Policies/ProjectPolicyTest.php b/tests/Unit/Policies/ProjectPolicyTest.php new file mode 100644 index 0000000..6a7186a --- /dev/null +++ b/tests/Unit/Policies/ProjectPolicyTest.php @@ -0,0 +1,23 @@ +create(); + $project = Project::factory()->for($user)->create(); + + expect((new ProjectPolicy())->view($user, $project)) + ->toBeTrue(); +}); + +it('cannot view a project that does not belong to the user', function () { + $user = User::factory()->create(); + $project = Project::factory()->create(); + + expect((new ProjectPolicy())->view($user, $project)) + ->toBeFalse(); +});