diff --git a/config/app.php b/config/app.php index 3cd08e40..6449154a 100644 --- a/config/app.php +++ b/config/app.php @@ -37,6 +37,7 @@ return [ \Engelsystem\Http\HttpClientServiceProvider::class, \Engelsystem\Helpers\DumpServerServiceProvider::class, \Engelsystem\Helpers\UuidServiceProvider::class, + \Engelsystem\Controllers\Api\UsesAuthServiceProvider::class, ], // Application middleware diff --git a/config/routes.php b/config/routes.php index ec0c558f..972163c6 100644 --- a/config/routes.php +++ b/config/routes.php @@ -130,9 +130,9 @@ $route->addGroup( $route->get('/news', 'Api\NewsController@index'); - $route->get('/users/self', 'Api\UsersController@self'); - $route->get('/users/{user_id:\d+}/angeltypes', 'Api\AngelTypeController@ofUser'); - $route->get('/users/{user_id:\d+}/shifts', 'Api\ShiftsController@entriesByUser'); + $route->get('/users/{user_id:(?:\d+|self)}', 'Api\UsersController@user'); + $route->get('/users/{user_id:(?:\d+|self)}/angeltypes', 'Api\AngelTypeController@ofUser'); + $route->get('/users/{user_id:(?:\d+|self)}/shifts', 'Api\ShiftsController@entriesByUser'); $route->addRoute( ['POST', 'PUT', 'DELETE', 'PATCH'], diff --git a/resources/api/openapi.yml b/resources/api/openapi.yml index 70b21dc3..32935d56 100644 --- a/resources/api/openapi.yml +++ b/resources/api/openapi.yml @@ -505,7 +505,17 @@ paths: '404': $ref: '#/components/responses/NotFoundError' - /users/self: + /users/{id}: + parameters: + - name: id + in: path + required: true + description: The user identifier or `self` + example: 42 + schema: + oneOf: + - type: string + - type: integer get: tags: - user @@ -519,7 +529,9 @@ paths: type: object properties: data: - $ref: '#/components/schemas/UserDetail' + anyOf: + - $ref: '#/components/schemas/UserDetail' + - $ref: '#/components/schemas/User' '401': $ref: '#/components/responses/UnauthorizedError' '403': @@ -532,10 +544,12 @@ paths: - name: id in: path required: true - description: The user identifier + description: The user identifier or `self` example: 42 schema: - type: integer + oneOf: + - type: string + - type: integer get: tags: - angeltype @@ -565,10 +579,12 @@ paths: - name: id in: path required: true - description: The user identifier + description: The user identifier or `self` example: 42 schema: - type: integer + oneOf: + - type: string + - type: integer get: tags: - shift diff --git a/src/Controllers/Api/AngelTypeController.php b/src/Controllers/Api/AngelTypeController.php index ed774770..296cf3c8 100644 --- a/src/Controllers/Api/AngelTypeController.php +++ b/src/Controllers/Api/AngelTypeController.php @@ -9,10 +9,11 @@ use Engelsystem\Controllers\Api\Resources\UserAngelTypeResource; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\AngelType; -use Engelsystem\Models\User\User; class AngelTypeController extends ApiController { + use UsesAuth; + public function index(): Response { $models = AngelType::query() @@ -26,9 +27,10 @@ class AngelTypeController extends ApiController public function ofUser(Request $request): Response { - $id = (int) $request->getAttribute('user_id'); - $model = User::findOrFail($id); - $data = ['data' => UserAngelTypeResource::collection($model->userAngelTypes)]; + $id = $request->getAttribute('user_id'); + $user = $this->getUser($id); + + $data = ['data' => UserAngelTypeResource::collection($user->userAngelTypes)]; return $this->response ->withContent(json_encode($data)); diff --git a/src/Controllers/Api/ShiftsController.php b/src/Controllers/Api/ShiftsController.php index 854d38c9..e30d4d36 100644 --- a/src/Controllers/Api/ShiftsController.php +++ b/src/Controllers/Api/ShiftsController.php @@ -15,11 +15,12 @@ use Engelsystem\Models\Location; use Engelsystem\Models\Shifts\NeededAngelType; use Engelsystem\Models\Shifts\Shift; use Engelsystem\Models\Shifts\ShiftEntry; -use Engelsystem\Models\User\User; use Illuminate\Database\Eloquent\Collection; class ShiftsController extends ApiController { + use UsesAuth; + public function entriesByAngeltype(Request $request): Response { $id = (int) $request->getAttribute('angeltype_id'); @@ -72,9 +73,9 @@ class ShiftsController extends ApiController public function entriesByUser(Request $request): Response { - $id = (int) $request->getAttribute('user_id'); - /** @var User $user */ - $user = User::findOrFail($id); + $id = $request->getAttribute('user_id'); + $user = $this->getUser($id); + /** @var ShiftEntry[]|Collection $shifts */ $shiftEntries = $user->shiftEntries() ->with([ diff --git a/src/Controllers/Api/UsersController.php b/src/Controllers/Api/UsersController.php index eeb1e26c..5173a128 100644 --- a/src/Controllers/Api/UsersController.php +++ b/src/Controllers/Api/UsersController.php @@ -5,21 +5,21 @@ declare(strict_types=1); namespace Engelsystem\Controllers\Api; use Engelsystem\Controllers\Api\Resources\UserDetailResource; -use Engelsystem\Helpers\Authenticator; +use Engelsystem\Controllers\Api\Resources\UserResource; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; class UsersController extends ApiController { - public function __construct(Response $response, protected Authenticator $auth) - { - parent::__construct($response); - } + use UsesAuth; - public function self(): Response + public function user(Request $request): Response { - $user = $this->auth->user(); + $id = $request->getAttribute('user_id'); + $user = $this->getUser($id); - $data = ['data' => (new UserDetailResource($user))->toArray()]; + $userData = $user->id == $this->auth->user()->id ? new UserDetailResource($user) : new UserResource($user); + $data = ['data' => $userData->toArray()]; return $this->response ->withContent(json_encode($data)); } diff --git a/src/Controllers/Api/UsesAuth.php b/src/Controllers/Api/UsesAuth.php new file mode 100644 index 00000000..cefaaeb9 --- /dev/null +++ b/src/Controllers/Api/UsesAuth.php @@ -0,0 +1,27 @@ +auth = $auth; + } + + protected function getUser(int|string $userId): ?User + { + if ($userId == 'self' && $this->auth) { + return $this->auth->user(); + } + + return User::findOrFail($userId); + } +} diff --git a/src/Controllers/Api/UsesAuthServiceProvider.php b/src/Controllers/Api/UsesAuthServiceProvider.php new file mode 100644 index 00000000..e0787350 --- /dev/null +++ b/src/Controllers/Api/UsesAuthServiceProvider.php @@ -0,0 +1,24 @@ +app->afterResolving(function ($object, Application $app): void { + if (!$object instanceof ApiController || !method_exists($object, 'setAuth')) { + return; + } + + /** @var UsesAuth $object */ + $object->setAuth($app->get(Authenticator::class)); + }); + } +} diff --git a/tests/Unit/Controllers/Api/AngelTypeControllerTest.php b/tests/Unit/Controllers/Api/AngelTypeControllerTest.php index dc161508..a4e7f63d 100644 --- a/tests/Unit/Controllers/Api/AngelTypeControllerTest.php +++ b/tests/Unit/Controllers/Api/AngelTypeControllerTest.php @@ -5,11 +5,13 @@ declare(strict_types=1); namespace Engelsystem\Test\Unit\Controllers\Api; use Engelsystem\Controllers\Api\AngelTypeController; +use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\AngelType; use Engelsystem\Models\User\User; use Engelsystem\Models\UserAngelType; +use Illuminate\Database\Eloquent\ModelNotFoundException; class AngelTypeControllerTest extends ApiBaseControllerTest { @@ -18,7 +20,6 @@ class AngelTypeControllerTest extends ApiBaseControllerTest */ public function testIndex(): void { - $this->initDatabase(); $items = AngelType::factory(3)->create(); $controller = new AngelTypeController(new Response()); @@ -42,7 +43,6 @@ class AngelTypeControllerTest extends ApiBaseControllerTest */ public function testOfUser(): void { - $this->initDatabase(); $user = User::factory()->create(); $items = UserAngelType::factory(3)->create(['user_id' => $user->id]); @@ -61,4 +61,38 @@ class AngelTypeControllerTest extends ApiBaseControllerTest return $item['name'] == $items->first()->angelType->name; })); } + + /** + * @covers \Engelsystem\Controllers\Api\AngelTypeController::ofUser + */ + public function testEntriesOfUserSelf(): void + { + $user = User::factory()->create(); + + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + + $request = new Request(); + $request = $request->withAttribute('user_id', 'self'); + + $controller = new AngelTypeController(new Response()); + $controller->setAuth($auth); + + $response = $controller->ofUser($request); + $this->validateApiResponse('/users/{id}/angeltypes', 'get', $response); + } + + /** + * @covers \Engelsystem\Controllers\Api\AngelTypeController::ofUser + */ + public function testEntriesByUserNotFound(): void + { + $request = new Request(); + $request = $request->withAttribute('user_id', 42); + + $controller = new AngelTypeController(new Response()); + + $this->expectException(ModelNotFoundException::class); + $controller->ofUser($request); + } } diff --git a/tests/Unit/Controllers/Api/ShiftsControllerTest.php b/tests/Unit/Controllers/Api/ShiftsControllerTest.php index 9c098930..ac5e40d7 100644 --- a/tests/Unit/Controllers/Api/ShiftsControllerTest.php +++ b/tests/Unit/Controllers/Api/ShiftsControllerTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Engelsystem\Test\Unit\Controllers\Api; use Engelsystem\Controllers\Api\ShiftsController; +use Engelsystem\Helpers\Authenticator; use Engelsystem\Helpers\Carbon; use Engelsystem\Http\Request; use Engelsystem\Http\Response; @@ -17,6 +18,7 @@ use Engelsystem\Models\Shifts\ShiftEntry; use Engelsystem\Models\User\Contact; use Engelsystem\Models\User\PersonalData; use Engelsystem\Models\User\User; +use Illuminate\Database\Eloquent\ModelNotFoundException; class ShiftsControllerTest extends ApiBaseControllerTest { @@ -133,6 +135,40 @@ class ShiftsControllerTest extends ApiBaseControllerTest $this->assertTrue(count($shift['entries']) >= 1); } + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByUser + */ + public function testEntriesByUserSelf(): void + { + $user = User::query()->first(); + + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + + $request = new Request(); + $request = $request->withAttribute('user_id', 'self'); + + $controller = new ShiftsController(new Response()); + $controller->setAuth($auth); + + $response = $controller->entriesByUser($request); + $this->validateApiResponse('/users/{id}/shifts', 'get', $response); + } + + /** + * @covers \Engelsystem\Controllers\Api\ShiftsController::entriesByUser + */ + public function testEntriesByUserNotFound(): void + { + $request = new Request(); + $request = $request->withAttribute('user_id', 42); + + $controller = new ShiftsController(new Response()); + + $this->expectException(ModelNotFoundException::class); + $controller->entriesByUser($request); + } + public function setUp(): void { parent::setUp(); diff --git a/tests/Unit/Controllers/Api/Stub/UsesAuthImplementation.php b/tests/Unit/Controllers/Api/Stub/UsesAuthImplementation.php new file mode 100644 index 00000000..75557847 --- /dev/null +++ b/tests/Unit/Controllers/Api/Stub/UsesAuthImplementation.php @@ -0,0 +1,19 @@ +getUser($id); + } +} diff --git a/tests/Unit/Controllers/Api/UsersControllerTest.php b/tests/Unit/Controllers/Api/UsersControllerTest.php index 0f99299e..65371ed5 100644 --- a/tests/Unit/Controllers/Api/UsersControllerTest.php +++ b/tests/Unit/Controllers/Api/UsersControllerTest.php @@ -6,6 +6,7 @@ namespace Engelsystem\Test\Unit\Controllers\Api; use Engelsystem\Controllers\Api\UsersController; use Engelsystem\Helpers\Authenticator; +use Engelsystem\Http\Request; use Engelsystem\Http\Response; use Engelsystem\Models\User\Contact; use Engelsystem\Models\User\PersonalData; @@ -17,11 +18,10 @@ use PHPUnit\Framework\MockObject\MockObject; class UsersControllerTest extends ApiBaseControllerTest { /** - * @covers \Engelsystem\Controllers\Api\UsersController::__construct - * @covers \Engelsystem\Controllers\Api\UsersController::self + * @covers \Engelsystem\Controllers\Api\UsersController::user * @covers \Engelsystem\Controllers\Api\Resources\UserDetailResource::toArray */ - public function testSelf(): void + public function testUser(): void { $user = User::factory() ->has(Contact::factory()) @@ -32,12 +32,16 @@ class UsersControllerTest extends ApiBaseControllerTest /** @var Authenticator|MockObject $auth */ $auth = $this->createMock(Authenticator::class); - $this->setExpects($auth, 'user', null, $user); + $this->setExpects($auth, 'user', null, $user, $this->atLeastOnce()); - $controller = new UsersController(new Response(), $auth); + $request = new Request(); + $request = $request->withAttribute('user_id', 'self'); - $response = $controller->self(); - $this->validateApiResponse('/users/self', 'get', $response); + $controller = new UsersController(new Response()); + $controller->setAuth($auth); + + $response = $controller->user($request); + $this->validateApiResponse('/users/{id}', 'get', $response); $this->assertEquals(['application/json'], $response->getHeader('content-type')); $this->assertJson($response->getContent()); @@ -48,4 +52,36 @@ class UsersControllerTest extends ApiBaseControllerTest $this->assertEquals($user->id, $data['data']['id']); $this->assertArrayHasKey('dates', $data['data']); } + + /** + * @covers \Engelsystem\Controllers\Api\UsersController::user + */ + public function testUserById(): void + { + $user = User::factory()->create(); + $otherUser = User::factory() + ->has(Contact::factory()) + ->has(PersonalData::factory()) + ->has(Settings::factory()) + ->has(State::factory()) + ->create(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user, $this->atLeastOnce()); + + $request = new Request(); + $request = $request->withAttribute('user_id', $otherUser->id); + + $controller = new UsersController(new Response()); + $controller->setAuth($auth); + + $response = $controller->user($request); + $this->validateApiResponse('/users/{id}', 'get', $response); + + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('id', $data['data']); + $this->assertEquals($otherUser->id, $data['data']['id']); + $this->assertArrayNotHasKey('dates', $data['data']); + } } diff --git a/tests/Unit/Controllers/Api/UsesAuthServiceProviderTest.php b/tests/Unit/Controllers/Api/UsesAuthServiceProviderTest.php new file mode 100644 index 00000000..0e24aa37 --- /dev/null +++ b/tests/Unit/Controllers/Api/UsesAuthServiceProviderTest.php @@ -0,0 +1,36 @@ +app); + $serviceProvider->register(); + + $user = new User(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + $this->app->instance(Authenticator::class, $auth); + + /** @var UsesAuthImplementation $instance */ + $instance = $this->app->make(UsesAuthImplementation::class); + + $this->assertEquals($user, $instance->user('self')); + } +} diff --git a/tests/Unit/Controllers/Api/UsesAuthTest.php b/tests/Unit/Controllers/Api/UsesAuthTest.php new file mode 100644 index 00000000..2ba4830f --- /dev/null +++ b/tests/Unit/Controllers/Api/UsesAuthTest.php @@ -0,0 +1,72 @@ +createInstance(); + + $this->expectException(ModelNotFoundException::class); + $usesAuth->user('self'); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsesAuth::getUser + */ + public function testGetUserNotFound(): void + { + $usesAuth = $this->createInstance(); + + $this->expectException(ModelNotFoundException::class); + $usesAuth->user(42); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsesAuth::getUser + */ + public function testGetUserWithoutAuth(): void + { + $user = User::factory()->create(); + + $usesAuth = $this->createInstance(); + + $this->assertInstanceOf(User::class, $usesAuth->user($user->id)); + } + + /** + * @covers \Engelsystem\Controllers\Api\UsesAuth::setAuth + * @covers \Engelsystem\Controllers\Api\UsesAuth::getUser + */ + public function testGetUser(): void + { + $user = User::factory()->create(); + + /** @var Authenticator|MockObject $auth */ + $auth = $this->createMock(Authenticator::class); + $this->setExpects($auth, 'user', null, $user); + + $usesAuth = $this->createInstance(); + $usesAuth->setAuth($auth); + + $this->assertEquals($user, $usesAuth->user('self')); + } + + protected function createInstance(): object + { + return new UsesAuthImplementation(new Response()); + } +}