From 21207a4058292e13a7f09416281f057f684f9ba5 Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Thu, 18 Jul 2024 10:00:20 +0200 Subject: [PATCH] Added me endpoints --- .../Controllers/Api/V1/MemberController.php | 7 +-- .../Controllers/Api/V1/UserController.php | 28 ++++++++++++ .../Api/V1/UserMemberController.php | 37 ++++++++++++++++ .../Resources/V1/Member/MemberCollection.php | 2 +- .../V1/Member/MemberPivotResource.php | 44 ------------------- .../V1/Member/PersonalMemberCollection.php | 18 ++++++++ .../V1/Member/PersonalMemberResource.php | 36 +++++++++++++++ app/Http/Resources/V1/User/UserResource.php | 39 ++++++++++++++++ routes/api.php | 12 +++++ .../Unit/Endpoint/Api/V1/UserEndpointTest.php | 43 ++++++++++++++++++ .../Api/V1/UserMemberEndpointTest.php | 43 ++++++++++++++++++ 11 files changed, 261 insertions(+), 48 deletions(-) create mode 100644 app/Http/Controllers/Api/V1/UserController.php create mode 100644 app/Http/Controllers/Api/V1/UserMemberController.php delete mode 100644 app/Http/Resources/V1/Member/MemberPivotResource.php create mode 100644 app/Http/Resources/V1/Member/PersonalMemberCollection.php create mode 100644 app/Http/Resources/V1/Member/PersonalMemberResource.php create mode 100644 app/Http/Resources/V1/User/UserResource.php create mode 100644 tests/Unit/Endpoint/Api/V1/UserEndpointTest.php create mode 100644 tests/Unit/Endpoint/Api/V1/UserMemberEndpointTest.php diff --git a/app/Http/Controllers/Api/V1/MemberController.php b/app/Http/Controllers/Api/V1/MemberController.php index 1d05c48b..563bc2c3 100644 --- a/app/Http/Controllers/Api/V1/MemberController.php +++ b/app/Http/Controllers/Api/V1/MemberController.php @@ -14,7 +14,6 @@ use App\Exceptions\Api\UserNotPlaceholderApiException; use App\Http\Requests\V1\Member\MemberIndexRequest; use App\Http\Requests\V1\Member\MemberUpdateRequest; use App\Http\Resources\V1\Member\MemberCollection; -use App\Http\Resources\V1\Member\MemberPivotResource; use App\Http\Resources\V1\Member\MemberResource; use App\Models\Member; use App\Models\Organization; @@ -40,7 +39,7 @@ class MemberController extends Controller /** * List all members of an organization * - * @return MemberCollection> + * @return MemberCollection * * @throws AuthorizationException * @@ -50,7 +49,9 @@ class MemberController extends Controller { $this->checkPermission($organization, 'members:view'); - $members = $organization->users() + $members = Member::query() + ->whereBelongsTo($organization, 'organization') + ->with(['user']) ->paginate(config('app.pagination_per_page_default')); return MemberCollection::make($members); diff --git a/app/Http/Controllers/Api/V1/UserController.php b/app/Http/Controllers/Api/V1/UserController.php new file mode 100644 index 00000000..547b6c1a --- /dev/null +++ b/app/Http/Controllers/Api/V1/UserController.php @@ -0,0 +1,28 @@ +user(); + + return new UserResource($user); + } +} diff --git a/app/Http/Controllers/Api/V1/UserMemberController.php b/app/Http/Controllers/Api/V1/UserMemberController.php new file mode 100644 index 00000000..ca858ba6 --- /dev/null +++ b/app/Http/Controllers/Api/V1/UserMemberController.php @@ -0,0 +1,37 @@ + + * + * @throws AuthorizationException + */ + public function myMembers(): JsonResource + { + $user = $this->user(); + + $members = Member::query() + ->whereBelongsTo($user, 'user') + ->with(['organization']) + ->get(); + + return new PersonalMemberCollection($members); + } +} diff --git a/app/Http/Resources/V1/Member/MemberCollection.php b/app/Http/Resources/V1/Member/MemberCollection.php index f23b882a..4bff7741 100644 --- a/app/Http/Resources/V1/Member/MemberCollection.php +++ b/app/Http/Resources/V1/Member/MemberCollection.php @@ -14,5 +14,5 @@ class MemberCollection extends ResourceCollection implements PaginatedResourceCo * * @var string */ - public $collects = MemberPivotResource::class; + public $collects = MemberResource::class; } diff --git a/app/Http/Resources/V1/Member/MemberPivotResource.php b/app/Http/Resources/V1/Member/MemberPivotResource.php deleted file mode 100644 index bfd1125f..00000000 --- a/app/Http/Resources/V1/Member/MemberPivotResource.php +++ /dev/null @@ -1,44 +0,0 @@ -> - */ - public function toArray(Request $request): array - { - /** @var Member $member */ - $member = $this->resource->getRelationValue('membership'); - - return [ - /** @var string $id ID of membership */ - 'id' => $member->id, - /** @var string $id ID of user */ - 'user_id' => $this->resource->id, - /** @var string $name Name */ - 'name' => $this->resource->name, - /** @var string $email Email */ - 'email' => $this->resource->email, - /** @var string $role Role */ - 'role' => $member->role, - /** @var bool $is_placeholder Placeholder user for imports, user might not really exist and does not know about this placeholder membership */ - 'is_placeholder' => $this->resource->is_placeholder, - /** @var int|null $billable_rate Billable rate in cents per hour */ - 'billable_rate' => $member->billable_rate, - ]; - } -} diff --git a/app/Http/Resources/V1/Member/PersonalMemberCollection.php b/app/Http/Resources/V1/Member/PersonalMemberCollection.php new file mode 100644 index 00000000..d00c0226 --- /dev/null +++ b/app/Http/Resources/V1/Member/PersonalMemberCollection.php @@ -0,0 +1,18 @@ +> + */ + public function toArray(Request $request): array + { + return [ + /** @var string $id ID of membership */ + 'id' => $this->resource->id, + 'organization' => [ + /** @var int $id ID of organization */ + 'id' => $this->resource->organization->id, + /** @var string $name Name of organization */ + 'name' => $this->resource->organization->name, + ], + /** @var string $role Role */ + 'role' => $this->resource->role, + ]; + } +} diff --git a/app/Http/Resources/V1/User/UserResource.php b/app/Http/Resources/V1/User/UserResource.php new file mode 100644 index 00000000..ad2525c1 --- /dev/null +++ b/app/Http/Resources/V1/User/UserResource.php @@ -0,0 +1,39 @@ +> + */ + public function toArray(Request $request): array + { + return [ + /** @var string $id ID of user */ + 'id' => $this->resource->id, + /** @var string $name Name of user */ + 'name' => $this->resource->name, + /** @var string $email Email of user */ + 'email' => $this->resource->email, + /** @var string $profile_photo_url Profile photo URL */ + 'profile_photo_url' => $this->resource->profile_photo_url, + /** @var string $timezone Timezone (f.e. Europe/Berlin or America/New_York) */ + 'timezone' => $this->resource->timezone, + /** @var Weekday $week_start Starting day of the week */ + 'week_start' => $this->resource->week_start->value, + ]; + } +} diff --git a/routes/api.php b/routes/api.php index 38823edf..d649248b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,6 +12,8 @@ use App\Http\Controllers\Api\V1\ProjectMemberController; use App\Http\Controllers\Api\V1\TagController; use App\Http\Controllers\Api\V1\TaskController; use App\Http\Controllers\Api\V1\TimeEntryController; +use App\Http\Controllers\Api\V1\UserController; +use App\Http\Controllers\Api\V1\UserMemberController; use App\Http\Controllers\Api\V1\UserTimeEntryController; use Illuminate\Support\Facades\Route; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -45,6 +47,16 @@ Route::middleware([ Route::post('/organizations/{organization}/members/{member}/invite-placeholder', [MemberController::class, 'invitePlaceholder'])->name('invite-placeholder'); }); + // User routes + Route::name('users.')->group(static function () { + Route::get('/users/me', [UserController::class, 'me'])->name('me'); + }); + + // User Member routes + Route::name('users.members.')->group(static function () { + Route::get('/users/me/members', [UserMemberController::class, 'myMembers'])->name('my-members'); + }); + // Invitation routes Route::name('invitations.')->group(static function () { Route::get('/organizations/{organization}/invitations', [InvitationController::class, 'index'])->name('index'); diff --git a/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php b/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php new file mode 100644 index 00000000..cd710f75 --- /dev/null +++ b/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php @@ -0,0 +1,43 @@ +getJson(route('api.v1.users.me')); + + // Assert + $response->assertUnauthorized(); + $response->assertJson(['message' => 'Unauthenticated.']); + } + + public function test_me_returns_information_about_the_current_user(): void + { + // Arrange + $data = $this->createUserWithPermission(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.users.me')); + + // Assert + $response->assertSuccessful(); + $response->assertJson([ + 'data' => [ + 'id' => $data->user->getKey(), + 'name' => $data->user->name, + 'email' => $data->user->email, + 'profile_photo_url' => $data->user->profile_photo_url, + 'timezone' => $data->user->timezone, + 'week_start' => $data->user->week_start->value, + ], + ]); + } +} diff --git a/tests/Unit/Endpoint/Api/V1/UserMemberEndpointTest.php b/tests/Unit/Endpoint/Api/V1/UserMemberEndpointTest.php new file mode 100644 index 00000000..544e4bf8 --- /dev/null +++ b/tests/Unit/Endpoint/Api/V1/UserMemberEndpointTest.php @@ -0,0 +1,43 @@ +getJson(route('api.v1.users.members.my-members')); + + // Assert + $response->assertUnauthorized(); + $response->assertJson(['message' => 'Unauthenticated.']); + } + + public function test_my_members_returns_information_about_the_organization_membership_of_the_current_user(): void + { + // Arrange + $data = $this->createUserWithPermission(); + $otherOrganization = Organization::factory()->create(); + $otherMember = Member::factory()->forOrganization($otherOrganization)->forUser($data->user)->create(); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.users.members.my-members')); + + // Assert + $response->assertSuccessful(); + $response->assertJsonCount(2, 'data'); + $otherMemberResponse = collect($response->json('data'))->where('id', '=', $otherMember->getKey())->first(); + $this->assertNotNull($otherMemberResponse); + $this->assertSame($otherMember->organization->getKey(), $otherMemberResponse['organization']['id']); + $memberResponse = collect($response->json('data'))->where('id', '=', $data->member->getKey())->first(); + $this->assertNotNull($memberResponse); + } +}