Skip to content

Commit 00528c3

Browse files
Add tests for events, event series, locations, organizations, personal access tokens, user roles; fix ability check for creating user roles and restriction to own personal access tokens
1 parent d9c7969 commit 00528c3

17 files changed

+454
-19
lines changed

app/Http/Controllers/UserRoleController.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use App\Http\Requests\Filters\UserRoleFilterRequest;
66
use App\Http\Requests\UserRoleRequest;
7-
use App\Models\User;
87
use App\Models\UserRole;
98
use Illuminate\Http\RedirectResponse;
109
use Illuminate\Support\Facades\Session;
@@ -29,7 +28,7 @@ public function index(UserRoleFilterRequest $request): View
2928

3029
public function create(): View
3130
{
32-
$this->authorize('create', User::class);
31+
$this->authorize('create', UserRole::class);
3332

3433
return view('user_roles.user_role_form');
3534
}

app/Models/PersonalAccessToken.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Models;
44

55
use Carbon\Carbon;
6+
use Illuminate\Database\Eloquent\Factories\HasFactory;
67
use Laravel\Sanctum\HasApiTokens;
78
use Laravel\Sanctum\NewAccessToken;
89
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
@@ -18,6 +19,8 @@
1819
*/
1920
class PersonalAccessToken extends SanctumPersonalAccessToken
2021
{
22+
use HasFactory;
23+
2124
public function fillAndSave(array $validatedData): bool
2225
{
2326
$this->fill($validatedData);

app/Models/UserRole.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Options\FilterValue;
1010
use Illuminate\Database\Eloquent\Builder;
1111
use Illuminate\Database\Eloquent\Collection;
12+
use Illuminate\Database\Eloquent\Factories\HasFactory;
1213
use Illuminate\Database\Eloquent\Model;
1314
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
1415
use Spatie\QueryBuilder\AllowedFilter;
@@ -26,6 +27,7 @@ class UserRole extends Model
2627
{
2728
use BuildsQueryFromRequest;
2829
use FiltersByRelationExistence;
30+
use HasFactory;
2931

3032
protected $casts = [
3133
'abilities' => 'json',

app/Policies/PersonalAccessTokenPolicy.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ public function viewOwn(User $user): Response
3434
*/
3535
public function view(User $user, PersonalAccessToken $personalAccessToken): Response
3636
{
37-
return $this->response(
38-
$user->is($personalAccessToken->tokenable)
39-
&& $user->hasAbility(Ability::ManagePersonalAccessTokens)
40-
);
37+
return $this->update($user, $personalAccessToken);
4138
}
4239

4340
/**
@@ -53,15 +50,18 @@ public function create(User $user): Response
5350
*/
5451
public function update(User $user, PersonalAccessToken $personalAccessToken): Response
5552
{
56-
return $this->requireAbility($user, Ability::ManagePersonalAccessTokens);
53+
return $this->response(
54+
$user->is($personalAccessToken->tokenable)
55+
&& $user->hasAbility(Ability::ManagePersonalAccessTokens)
56+
);
5757
}
5858

5959
/**
6060
* Determine whether the user can delete the model.
6161
*/
6262
public function delete(User $user, PersonalAccessToken $personalAccessToken): Response
6363
{
64-
return $this->requireAbility($user, Ability::ManagePersonalAccessTokens);
64+
return $this->update($user, $personalAccessToken);
6565
}
6666

6767
/**
@@ -77,6 +77,6 @@ public function restore(User $user, PersonalAccessToken $personalAccessToken): R
7777
*/
7878
public function forceDelete(User $user, PersonalAccessToken $personalAccessToken): Response
7979
{
80-
return $this->requireAbility($user, Ability::ManagePersonalAccessTokens);
80+
return $this->update($user, $personalAccessToken);
8181
}
8282
}

database/factories/EventFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,11 @@ public function definition(): array
3434
'website_url' => fake()->optional()->url(),
3535
];
3636
}
37+
38+
public function visibility(Visibility $visibility): static
39+
{
40+
return $this->state(fn (array $attributes) => [
41+
'visibility' => $visibility,
42+
]);
43+
}
3744
}

database/factories/EventSeriesFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,11 @@ public function definition(): array
2727
'visibility' => fake()->randomElement(Visibility::values()),
2828
];
2929
}
30+
31+
public function visibility(Visibility $visibility): static
32+
{
33+
return $this->state(fn (array $attributes) => [
34+
'visibility' => $visibility,
35+
]);
36+
}
3037
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Database\Factories;
4+
5+
use App\Models\PersonalAccessToken;
6+
use App\Options\Ability;
7+
use Illuminate\Database\Eloquent\Factories\Factory;
8+
9+
/**
10+
* @extends Factory<PersonalAccessToken>
11+
*/
12+
class PersonalAccessTokenFactory extends Factory
13+
{
14+
protected $model = PersonalAccessToken::class;
15+
16+
public function definition(): array
17+
{
18+
return [
19+
'name' => $this->faker->word(),
20+
'token' => $this->faker->sha256(),
21+
'abilities' => $this->faker->randomElements(Ability::cases(), $this->faker->numberBetween(1, count(Ability::cases()))),
22+
];
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Database\Factories;
4+
5+
use App\Models\UserRole;
6+
use App\Options\Ability;
7+
use Illuminate\Database\Eloquent\Factories\Factory;
8+
9+
/**
10+
* @extends Factory<UserRole>
11+
*/
12+
class UserRoleFactory extends Factory
13+
{
14+
/**
15+
* Define the model's default state.
16+
*
17+
* @return array<string, mixed>
18+
*/
19+
public function definition(): array
20+
{
21+
return [
22+
'name' => fake()->unique()->words(3, true),
23+
'abilities' => fake()->randomElements(Ability::cases(), $this->faker->numberBetween(1, count(Ability::cases()))),
24+
];
25+
}
26+
}

routes/web.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101

102102
Route::model('organization', Organization::class);
103103
Route::resource('organizations', OrganizationController::class)
104-
->only(['index', 'create', 'store', 'edit', 'update']);
104+
->only(['index', 'create', 'store', 'show', 'edit', 'update']);
105105
Route::prefix('organizations/{organization}')->group(function () {
106106
Route::post('documents', [DocumentController::class, 'storeForOrganization'])
107107
->name('organizations.documents.store');
@@ -138,8 +138,6 @@
138138
->only(['show']);
139139
Route::resource('event-series', EventSeriesController::class)
140140
->only(['show']);
141-
Route::resource('organizations', OrganizationController::class)
142-
->only(['show']);
143141

144142
Route::model('booking_option', BookingOption::class);
145143
Route::resource('events/{event:slug}/booking-options', BookingOptionController::class)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace Tests\Feature\Http;
4+
5+
use App\Http\Controllers\EventController;
6+
use App\Models\Event;
7+
use App\Models\Location;
8+
use App\Options\Ability;
9+
use App\Options\Visibility;
10+
use Illuminate\Foundation\Testing\RefreshDatabase;
11+
use PHPUnit\Framework\Attributes\CoversClass;
12+
use PHPUnit\Framework\Attributes\DataProvider;
13+
use Tests\TestCase;
14+
use Tests\Traits\ChecksVisibility;
15+
16+
#[CoversClass(EventController::class)]
17+
class EventControllerTest extends TestCase
18+
{
19+
use ChecksVisibility;
20+
use RefreshDatabase;
21+
22+
public function testEventsCanBeListedWithCorrectAbility(): void
23+
{
24+
$this->assertRouteOnlyAccessibleOnlyWithAbility('/events', Ability::ViewEvents);
25+
}
26+
27+
public function testPublicEventIsAccessibleByEveryone(): void
28+
{
29+
$publicEvent = $this->createRandomEvent(Visibility::Public);
30+
$route = "/events/{$publicEvent->slug}";
31+
32+
$this->assertRouteAccessibleAsGuest($route);
33+
$this->assertRouteAccessibleWithAbility($route, Ability::ViewEvents);
34+
$this->assertRouteAccessibleWithAbility($route, Ability::ViewPrivateEvents);
35+
}
36+
37+
public function testPrivateEventIsOnlyAccessibleWithCorrectAbility(): void
38+
{
39+
$privateEvent = $this->createRandomEvent(Visibility::Private);
40+
$route = "/events/{$privateEvent->slug}";
41+
42+
$this->assertRouteForbiddenAsGuest($route);
43+
$this->assertRouteNotAccessibleWithoutAbility($route, Ability::ViewPrivateEvents);
44+
$this->assertRouteAccessibleWithAbility($route, Ability::ViewPrivateEvents);
45+
}
46+
47+
public function testCreateEventFormIsOnlyAccessibleWithCorrectAbility(): void
48+
{
49+
$this->assertRouteOnlyAccessibleOnlyWithAbility('/events/create', Ability::CreateEvents);
50+
}
51+
52+
#[DataProvider('visibilityProvider')]
53+
public function testEditEventFormIsAccessibleOnlyWithCorrectAbility(Visibility $visibility): void
54+
{
55+
$this->assertRouteOnlyAccessibleOnlyWithAbility("/events/{$this->createRandomEvent($visibility)->slug}/edit", Ability::EditEvents);
56+
}
57+
58+
private function createRandomEvent(Visibility $visibility): Event
59+
{
60+
return Event::factory()
61+
->for(Location::factory()->create())
62+
->visibility($visibility)
63+
->create();
64+
}
65+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
namespace Tests\Feature\Http;
4+
5+
use App\Http\Controllers\EventSeriesController;
6+
use App\Models\Event;
7+
use App\Models\EventSeries;
8+
use App\Models\Location;
9+
use App\Options\Ability;
10+
use App\Options\Visibility;
11+
use Illuminate\Foundation\Testing\RefreshDatabase;
12+
use PHPUnit\Framework\Attributes\CoversClass;
13+
use PHPUnit\Framework\Attributes\DataProvider;
14+
use Tests\TestCase;
15+
use Tests\Traits\ChecksVisibility;
16+
17+
#[CoversClass(EventSeriesController::class)]
18+
class EventSeriesControllerTest extends TestCase
19+
{
20+
use ChecksVisibility;
21+
use RefreshDatabase;
22+
23+
public function testEventSeriesCanBeListedWithCorrectAbility(): void
24+
{
25+
$this->assertRouteOnlyAccessibleOnlyWithAbility('/event-series', Ability::ViewEventSeries);
26+
}
27+
28+
public function testPublicEventSeriesIsAccessibleByEveryone(): void
29+
{
30+
$publicEventSeries = $this->createRandomEventSeries(Visibility::Public);
31+
$route = "/event-series/{$publicEventSeries->slug}";
32+
33+
$this->assertRouteAccessibleAsGuest($route);
34+
$this->assertRouteAccessibleWithAbility($route, Ability::ViewEventSeries);
35+
$this->assertRouteAccessibleWithAbility($route, Ability::ViewPrivateEventSeries);
36+
}
37+
38+
public function testPrivateEventSeriesIsOnlyAccessibleWithCorrectAbility(): void
39+
{
40+
$privateEvent = $this->createRandomEventSeries(Visibility::Private);
41+
$route = "/event-series/{$privateEvent->slug}";
42+
43+
$this->assertRouteForbiddenAsGuest($route);
44+
$this->assertRouteNotAccessibleWithoutAbility($route, Ability::ViewPrivateEventSeries);
45+
$this->assertRouteAccessibleWithAbility($route, Ability::ViewPrivateEventSeries);
46+
}
47+
48+
public function testCreateEventSeriesFormIsOnlyAccessibleWithCorrectAbility(): void
49+
{
50+
$this->assertRouteOnlyAccessibleOnlyWithAbility('/event-series/create', Ability::CreateEventSeries);
51+
}
52+
53+
#[DataProvider('visibilityProvider')]
54+
public function testEditEventSeriesFormIsAccessibleOnlyWithCorrectAbility(Visibility $visibility): void
55+
{
56+
$this->assertRouteOnlyAccessibleOnlyWithAbility("/event-series/{$this->createRandomEventSeries($visibility)->slug}/edit", Ability::EditEventSeries);
57+
}
58+
59+
private function createRandomEventSeries(Visibility $visibility): EventSeries
60+
{
61+
return EventSeries::factory()
62+
->has(
63+
Event::factory()
64+
->for(Location::factory()->create())
65+
->count(fake()->numberBetween(1, 5))
66+
)
67+
->visibility($visibility)
68+
->create();
69+
}
70+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Http;
4+
5+
use App\Http\Controllers\LocationController;
6+
use App\Models\Location;
7+
use App\Options\Ability;
8+
use Illuminate\Foundation\Testing\RefreshDatabase;
9+
use PHPUnit\Framework\Attributes\CoversClass;
10+
use Tests\TestCase;
11+
12+
#[CoversClass(LocationController::class)]
13+
class LocationControllerTest extends TestCase
14+
{
15+
use RefreshDatabase;
16+
17+
public function testLocationsCanBeListedWithCorrectAbility(): void
18+
{
19+
$this->assertRouteOnlyAccessibleOnlyWithAbility('/locations', Ability::ViewLocations);
20+
}
21+
22+
public function testCreateLocationFormIsOnlyAccessibleWithCorrectAbility(): void
23+
{
24+
$this->assertRouteOnlyAccessibleOnlyWithAbility('/locations/create', Ability::CreateLocations);
25+
}
26+
27+
public function testEditLocationFormIsAccessibleOnlyWithCorrectAbility(): void
28+
{
29+
$this->assertRouteOnlyAccessibleOnlyWithAbility("/locations/{$this->createRandomLocation()->id}/edit", Ability::EditLocations);
30+
}
31+
32+
private function createRandomLocation(): Location
33+
{
34+
return Location::factory()->create();
35+
}
36+
}

0 commit comments

Comments
 (0)