Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Commit 5da9521

Browse files
authored
Merge pull request #155 from KA-Huis/feature/SM-105
SM-105: Create endpoint to retrieve a user
2 parents 1ce70e1 + 307b5eb commit 5da9521

File tree

10 files changed

+245
-6
lines changed

10 files changed

+245
-6
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\API\V1\Http\Controllers;
6+
7+
use App\API\V1\Http\Resources\PrivateUserResource;
8+
use App\API\V1\Http\Resources\PublicUserResource;
9+
use App\Http\Controllers\Controller;
10+
use App\Models\User;
11+
use Illuminate\Auth\Access\AuthorizationException;
12+
use Illuminate\Http\Request;
13+
14+
final class UserController extends Controller
15+
{
16+
/**
17+
* @throws AuthorizationException
18+
*/
19+
public function show(Request $request, User $user): PublicUserResource|PrivateUserResource
20+
{
21+
$this->authorize('view', $user);
22+
23+
if ($request->user()->can('viewPrivateProfile')) {
24+
return new PrivateUserResource($user);
25+
}
26+
27+
return new PublicUserResource($user);
28+
}
29+
}

app/API/V1/Http/Resources/UserResource.php renamed to app/API/V1/Http/Resources/PrivateUserResource.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use Illuminate\Http\Resources\Json\JsonResource;
88

9-
class UserResource extends JsonResource
9+
class PrivateUserResource extends JsonResource
1010
{
1111
/**
1212
* Transform the resource into an array.
@@ -18,6 +18,7 @@ class UserResource extends JsonResource
1818
public function toArray($request)
1919
{
2020
return [
21+
'id' => $this->id,
2122
'first_name' => $this->first_name,
2223
'last_name' => $this->last_name,
2324
'email' => $this->email,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\API\V1\Http\Resources;
6+
7+
use Illuminate\Http\Resources\Json\JsonResource;
8+
9+
class PublicUserResource extends JsonResource
10+
{
11+
/**
12+
* Transform the resource into an array.
13+
*
14+
* @param \Illuminate\Http\Request $request
15+
*
16+
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
17+
*/
18+
public function toArray($request)
19+
{
20+
return [
21+
'id' => $this->id,
22+
'first_name' => $this->first_name,
23+
'last_name' => $this->last_name,
24+
'created_at' => $this->created_at,
25+
'updated_at' => $this->updated_at,
26+
'deleted_at' => $this->deleted_at,
27+
];
28+
}
29+
}

app/API/V1/Http/Resources/ReparationRequestResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function toArray($request)
2626
'updated_at' => $this->updated_at,
2727
'deleted_at' => $this->deleted_at,
2828
'reporter_id' => $this->reporter_id,
29-
'reporter' => new UserResource($this->whenLoaded('reporter')),
29+
'reporter' => new PublicUserResource($this->whenLoaded('reporter')),
3030
'statuses' => new ReparationRequestStatusCollection($this->whenLoaded('statuses')),
3131
'materials' => new ReparationRequestMaterialCollection($this->whenLoaded('materials')),
3232
];

app/API/V1/Http/Resources/ReservationResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function toArray($request)
2727
'updated_at' => $this->updated_at,
2828
'deleted_at' => $this->deleted_at,
2929
'created_by_user_id' => $this->created_by_user_id,
30-
'created_by_user' => new UserResource($this->whenLoaded('createdByUser')),
30+
'created_by_user' => new PublicUserResource($this->whenLoaded('createdByUser')),
3131
'space' => new SpaceResource($this->whenLoaded('space')),
3232
'group' => new GroupResource($this->whenLoaded('group')),
3333
];

app/Policies/UserPolicy.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ public function view(User $user, User $userSubject): bool
2727
return true;
2828
}
2929

30+
/**
31+
* Determine whether the user can view the model.
32+
*/
33+
public function viewPrivateProfile(User $user, User $userSubject): bool
34+
{
35+
return $user->id === $userSubject->id;
36+
}
37+
3038
/**
3139
* Determine whether the user can create models.
3240
*/

openapi/openapi-v1.yml

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ components:
2222
scopes: { }
2323

2424
schemas:
25-
User:
25+
PublicUserProfile:
2626
type: object
2727
properties:
2828
id:
@@ -47,6 +47,33 @@ components:
4747
- first_nane
4848
- last_name
4949
- email
50+
PrivateUserProfile:
51+
type: object
52+
properties:
53+
id:
54+
type: integer
55+
format: bigint20
56+
minimum: 1
57+
first_name:
58+
type: string
59+
last_name:
60+
type: string
61+
email:
62+
type: string
63+
created_at:
64+
type: string
65+
format: date-time
66+
updated_at:
67+
type: string
68+
format: date-time
69+
deleted_at:
70+
type: string
71+
format: date-time
72+
required:
73+
- id
74+
- first_nane
75+
- last_name
76+
- email
5077
Reservation:
5178
type: object
5279
properties:
@@ -84,7 +111,7 @@ components:
84111
created_by_user:
85112
readOnly: true
86113
allOf:
87-
- $ref: '#/components/schemas/User'
114+
- $ref: '#/components/schemas/PublicUserProfile'
88115
space:
89116
readOnly: true
90117
allOf:
@@ -213,7 +240,7 @@ components:
213240
reporter:
214241
readOnly: true
215242
allOf:
216-
- $ref: '#/components/schemas/User'
243+
- $ref: '#/components/schemas/PublicUserProfile'
217244
required:
218245
- id
219246
- title
@@ -1085,3 +1112,31 @@ paths:
10851112
type: object
10861113
403:
10871114
$ref: '#/components/responses/UnauthorizedResponse'
1115+
1116+
/users/{id}:
1117+
parameters:
1118+
- in: path
1119+
name: id
1120+
description: The user ID
1121+
required: true
1122+
schema:
1123+
type: integer
1124+
minimum: 1
1125+
get:
1126+
tags:
1127+
- Users
1128+
summary: Get user
1129+
description: Retrieve a single user by its id.
1130+
responses:
1131+
200:
1132+
description: Succcessful operation
1133+
content:
1134+
application/json:
1135+
schema:
1136+
oneOf:
1137+
- $ref: '#/components/schemas/PublicUserProfile'
1138+
- $ref: '#/components/schemas/PrivateUserProfile'
1139+
403:
1140+
$ref: '#/components/responses/UnauthorizedResponse'
1141+
422:
1142+
$ref: '#/components/responses/ValidationErrorResponse'

routes/api/v1.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\API\V1\Http\Controllers\ReparationRequestMaterialController;
1010
use App\API\V1\Http\Controllers\ReservationController;
1111
use App\API\V1\Http\Controllers\SpaceController;
12+
use App\API\V1\Http\Controllers\UserController;
1213
use App\Authentication\Guards\RestApiGuard;
1314
use Illuminate\Routing\Router;
1415

@@ -73,4 +74,11 @@
7374
$router->get('/{group}', [GroupController::class, 'show'])->name('show');
7475
$router->delete('/{group}', [GroupController::class, 'destroy'])->name('destroy');
7576
});
77+
78+
$router
79+
->prefix('users')
80+
->name('user.')
81+
->group(static function (Router $router) {
82+
$router->get('/{user}', [UserController::class, 'show'])->name('show');
83+
});
7684
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Feature\API\V1\Http\Controllers;
6+
7+
use App\Authentication\Guards\RestApiGuard;
8+
use App\Models\User;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use Illuminate\Routing\UrlGenerator;
11+
use Illuminate\Testing\Fluent\AssertableJson;
12+
use Tests\TestCase;
13+
14+
class UserControllerTest extends TestCase
15+
{
16+
use RefreshDatabase;
17+
18+
private UrlGenerator $urlGenerator;
19+
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->urlGenerator = $this->app->get(UrlGenerator::class);
25+
}
26+
27+
public function testShowEndpoint(): void
28+
{
29+
// Given
30+
$user = User::factory()->create();
31+
32+
$expectedUser = User::factory()
33+
->create();
34+
35+
$endpointUri = $this->urlGenerator->route('api.v1.user.show', [
36+
'user' => $expectedUser->id,
37+
]);
38+
39+
// When
40+
$response = $this
41+
->actingAs($user, (new RestApiGuard())->getName())
42+
->get($endpointUri);
43+
44+
// Then
45+
$response->assertOk()
46+
->assertJson(
47+
fn (AssertableJson $json) => $json
48+
->has('data',
49+
fn (AssertableJson $json) => $json
50+
->where('id', $expectedUser->id)
51+
->etc()
52+
)
53+
->etc()
54+
);
55+
}
56+
}

tests/Unit/Policies/UserPolicyTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,59 @@ public function testView(): void
5050
self::assertTrue($response);
5151
}
5252

53+
/**
54+
* This tests the `viewPrivateProfile` action.
55+
*
56+
* @testdox This verifies the happy flow when someone is allowed to view a reparation request.
57+
*/
58+
public function testViewPrivateProfile(): void
59+
{
60+
// Given
61+
$policy = new UserPolicy();
62+
63+
$user = Mockery::mock(User::class);
64+
$user->shouldReceive('getAttribute')
65+
->with('id')
66+
->andReturn(1)
67+
->twice();
68+
69+
// When
70+
$response = $policy->viewPrivateProfile($user, $user);
71+
72+
// Then
73+
self::assertTrue($response);
74+
}
75+
76+
/**
77+
* This tests the `viewPrivateProfile` action.
78+
*
79+
* @testdox This verifies the flow when someone is not allowed to view a private user profile, because it's not its
80+
* own profile.
81+
*/
82+
public function testViewPrivateProfileWhenSubjectIsDifferentThanAuthenticatedUser(): void
83+
{
84+
// Given
85+
$policy = new UserPolicy();
86+
87+
$user = Mockery::mock(User::class);
88+
$user->shouldReceive('getAttribute')
89+
->with('id')
90+
->andReturn(1)
91+
->once();
92+
93+
$userSubject = Mockery::mock(User::class);
94+
$userSubject->shouldReceive('getAttribute')
95+
->with('id')
96+
->andReturn(2)
97+
->once();
98+
99+
// When
100+
$response = $policy->viewPrivateProfile($user, $userSubject);
101+
102+
// Then
103+
self::assertFalse($response);
104+
}
105+
53106
/**
54107
* This tests the `create` action.
55108
*

0 commit comments

Comments
 (0)