Commit 8a44c180 authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

create groups endpoint

parent 8ffbbed6
......@@ -3,7 +3,9 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Source\Authorization\Authorizer;
abstract class ApiController extends Controller
{
......@@ -12,6 +14,22 @@ abstract class ApiController extends Controller
*/
protected int $status = 200;
/**
* @var \Illuminate\Http\Request
*/
protected Request $request;
/**
* @var \Source\Authorization\Authorizer
*/
protected Authorizer $authorizer;
public function __construct(Request $request, Authorizer $authorizer)
{
$this->request = $request;
$this->authorizer = $authorizer;
}
/**
* @param int $code
*/
......
<?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Source\Authorization\Permissions;
use Source\UseCases\Groups\CreateGroup\APIPresenter as CreateGroupAPIPresenter;
use Source\UseCases\Groups\CreateGroup\CreateGroupUseCase;
class GroupsController extends ApiController
{
/**
* @param \Source\UseCases\Groups\CreateGroup\CreateGroupUseCase $useCase
* @return \Illuminate\Http\JsonResponse
* @throws \Source\Exceptions\AuthorizationException
* @throws \Source\Exceptions\EntityNotFoundException
* @throws \Illuminate\Validation\ValidationException
* @throws \Source\Exceptions\EntityExistsException
*/
public function store(CreateGroupUseCase $useCase): JsonResponse
{
$this->authorizer->protectAll([Permissions::MANAGE_GROUPS]);
$this->validate($this->request, [
'title' => 'required|string|max:255',
'description' => 'required|string'
]);
$presenter = new CreateGroupAPIPresenter();
$useCase->create($this->request->all(), $presenter);
return $this->respondWithData($presenter->getViewModel());
}
}
......@@ -3,9 +3,7 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Source\Authorization\Authorizer;
use Source\Authorization\Permissions;
use Source\UseCases\Users\GetUser\GetUserUseCase;
use Source\UseCases\Users\CreateUser\CreateUserUseCase;
......@@ -21,26 +19,6 @@ use Source\UseCases\Users\UpdateUser\APIPresenter as UpdateUserAPIPresenter;
class UsersController extends ApiController
{
/**
* @var \Illuminate\Http\Request
*/
protected Request $request;
/**
* @var \Source\Authorization\Authorizer
*/
protected Authorizer $authorizer;
/**
* @param \Illuminate\Http\Request $request
* @param \Source\Authorization\Authorizer $authorizer
*/
public function __construct(Request $request, Authorizer $authorizer)
{
$this->request = $request;
$this->authorizer = $authorizer;
}
/**
* @param \Source\UseCases\Users\GetAllUsers\GetAllUsersUseCase $getAllUsers
* @return \Illuminate\Http\JsonResponse
......@@ -88,19 +66,16 @@ class UsersController extends ApiController
{
$this->authorizer->protectAll([Permissions::MANAGE_USERS]);
$this->validate(
$this->request,
[
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'display_name' => 'required|string|max:255',
'emplid' => 'nullable|string|max:7|min:6',
'email' => 'required|email|max:255',
'password' => 'nullable|string|min:15|max:255',
'doorcode' => 'required|string|numeric|digits_between:4,255',
'expires_at' => 'nullable|string|date|max:255',
]
);
$this->validate($this->request, [
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'display_name' => 'required|string|max:255',
'emplid' => 'nullable|string|max:7|min:6',
'email' => 'required|email|max:255',
'password' => 'nullable|string|min:15|max:255',
'doorcode' => 'required|string|numeric|digits_between:4,255',
'expires_at' => 'nullable|string|date|max:255',
]);
$presenter = new CreateUserAPIPresenter();
......@@ -124,14 +99,14 @@ class UsersController extends ApiController
$this->validate(
$this->request,
[
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'display_name' => 'required|string|max:255',
'emplid' => 'nullable|string|max:7|min:6',
'email' => 'required|email|max:255',
'password' => 'nullable|string|max:255',
'doorcode' => 'nullable|string|numeric|digits_between:4,255',
'expires_at' => 'nullable|string|date|max:255',
'emplid' => 'nullable|string|max:7|min:6',
'email' => 'required|email|max:255',
'password' => 'nullable|string|max:255',
'doorcode' => 'nullable|string|numeric|digits_between:4,255',
'expires_at' => 'nullable|string|date|max:255',
]
);
......
......@@ -15,6 +15,7 @@ use Source\UseCases\Users\CreateUser\CreateUserUseCaseServiceProvider;
use Source\UseCases\Users\DeleteUser\DeleteUserUseCaseServiceProvider;
use Source\UseCases\Users\UpdateUser\UpdateUserUseCaseServiceProvider;
use Source\UseCases\Users\GetAllUsers\GetAllUsersUseCaseServiceProvider;
use Source\UseCases\Groups\CreateGroup\CreateGroupUseCaseServiceProvider;
use Source\UseCases\Token\Authenticate\AuthenticateUseCaseServiceProvider;
use Source\UseCases\Doors\Authenticate\AuthenticateUseCaseServiceProvider as DoorAuthenticateUseCaseServiceProvider;
use Source\UseCases\Users\Authenticate\AuthenticateUseCaseServiceProvider as UserAuthenticateUseCaseServiceProvider;
......@@ -38,12 +39,17 @@ class AppServiceProvider extends ServiceProvider
* @var string[]
*/
protected array $useCaseProviders = [
// Users
GetUserUseCaseServiceProvider::class,
DeleteUserUseCaseServiceProvider::class,
UpdateUserUseCaseServiceProvider::class,
CreateUserUseCaseServiceProvider::class,
GetAllUsersUseCaseServiceProvider::class,
// Groups
CreateGroupUseCaseServiceProvider::class,
// Doors
AuthenticateUseCaseServiceProvider::class,
DoorAuthenticateUseCaseServiceProvider::class,
UserAuthenticateUseCaseServiceProvider::class,
......
......@@ -4,6 +4,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\UsersController;
use App\Http\Controllers\GroupsController;
/*
|--------------------------------------------------------------------------
......@@ -30,6 +31,16 @@ Route::group(['middleware' => 'auth:api'], static function () {
Route::delete('{userId}', [UsersController::class, 'delete']);
});
Route::group([
'prefix' => 'groups',
], static function () {
Route::get('/', [GroupsController::class, 'index']);
Route::post('/', [GroupsController::class, 'store']);
Route::get('{groupId}', [GroupsController::class, 'get']);
Route::put('{groupId}', [GroupsController::class, 'update']);
Route::delete('{groupId', [GroupsController::class, 'delete']);
});
Route::get('/user', static function (Request $request) {
return $request->user();
});
......
......@@ -10,5 +10,6 @@ class Permissions
public const MANAGE_DOORS = 'manage-doors';
public const MANAGE_TOKEN = 'manage-token';
public const TOKEN_CREATE = 'token-create';
public const MANAGE_GROUPS = 'manage-groups';
public const CODE_QUERY = 'code-query';
}
......@@ -24,6 +24,7 @@ class LocalGroupUserRepository extends InMemoryGroupUserRepository
$this->addUserToGroup(LocalUsersRepository::getSemiPrivilegedUser()->getId(), LocalGroupsRepository::getManageTokenGroup()->getId());
$this->addUserToGroup(LocalUsersRepository::getSemiPrivilegedUser()->getId(), LocalGroupsRepository::getTokenCreateGroup()->getId());
$this->addUserToGroup(LocalUsersRepository::getSemiPrivilegedUser()->getId(), LocalGroupsRepository::getCodeQueryGroup()->getId());
$this->addUserToGroup(LocalUsersRepository::getSemiPrivilegedUser()->getId(), LocalGroupsRepository::getManageGroupsGroup()->getId());
$this->addUserToGroup(
LocalUsersRepository::getComputerScienceStudent()->getId(),
......
......@@ -25,6 +25,8 @@ class LocalGroupsRepository extends InMemoryGroupsRepository
$this->create(static::getComputerScienceMajorGroup());
$this->create(static::getEngineeringLabAccessGroup());
$this->create(static::getManageGroupsGroup());
}
/**
......@@ -122,4 +124,13 @@ class LocalGroupsRepository extends InMemoryGroupsRepository
'Gives access to the Electrical Engineering Labs'
);
}
public static function getManageGroupsGroup(): Group
{
return new Group(
9,
Permissions::MANAGE_GROUPS,
'Gives permission to manage stored groups'
);
}
}
......@@ -5,6 +5,7 @@ namespace Source\UseCases;
use Carbon\Carbon;
use Source\Entities\User;
use Source\Entities\Group;
abstract class BasePresenter
{
......@@ -70,6 +71,17 @@ abstract class BasePresenter
];
}
public function formatGroup(Group $group): array
{
return [
'id' => $group->getId(),
'title' => $group->getTitle(),
'description' => $group->getDescription(),
'created_at' => $this->formatDateTime($group->getCreatedAt()),
'updated_at' => $this->formatDateTime($group->getUpdatedAt()),
];
}
/**
* @param Carbon|null $date
* @param string $format
......
<?php
namespace Source\UseCases\Groups\CreateGroup;
use Source\UseCases\BasePresenter;
class APIPresenter extends BasePresenter implements Presenter
{
protected array $viewModel = [];
/** @inheritDoc */
public function present(ResponseModel $responseModel): void
{
$this->viewModel['group'] = $this->formatGroup($responseModel->getGroup());
}
/** @inheritDoc */
public function getViewModel(): array
{
return $this->viewModel;
}
}
<?php
namespace Source\UseCases\Groups\CreateGroup;
use Source\Entities\Group;
use Source\Gateways\Groups\GroupsRepository;
use Source\Exceptions\EntityExistsException;
class CreateGroup implements CreateGroupUseCase
{
/**
* @var \Source\Gateways\Groups\GroupsRepository
*/
protected GroupsRepository $groups;
public function __construct(GroupsRepository $groups)
{
$this->groups = $groups;
}
/**
* @inheritDoc
*/
public function create(array $attributes, Presenter $presenter): void
{
$group = new Group(
0,
$attributes['title'],
$attributes['description']
);
if (!($group = $this->groups->create($group))) {
throw new EntityExistsException();
}
$response = new ResponseModel($group);
$presenter->present($response);
}
}
<?php
namespace Source\UseCases\Groups\CreateGroup;
interface CreateGroupUseCase
{
/**
* Required attributes:
* title
* description
*
* @param array $attributes
* @param \Source\UseCases\Groups\CreateGroup\Presenter $presenter
* @throws \Source\Exceptions\EntityExistsException
*/
public function create(array $attributes, Presenter $presenter): void;
}
<?php
namespace Source\UseCases\Groups\CreateGroup;
use Source\Gateways\Groups\GroupsRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
/**
* Service provider must be registered in AppServiceProvider
*/
class CreateGroupUseCaseServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind(CreateGroupUseCase::class, static function (Application $app) {
return new CreateGroup($app->make(GroupsRepository::class));
});
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
}
/**
* @return array
*/
public function provides()
{
return [CreateGroupUseCase::class];
}
}
<?php
namespace Source\UseCases\Groups\CreateGroup;
interface Presenter
{
/**
* @param ResponseModel $responseModel
* @return void
*/
public function present(ResponseModel $responseModel): void;
/**
* @return array
*/
public function getViewModel(): array;
}
<?php
namespace Source\UseCases\Groups\CreateGroup;
use Source\Entities\Group;
class ResponseModel
{
/**
* @var \Source\Entities\Group
*/
protected Group $group;
/**
* @param \Source\Entities\Group $group
*/
public function __construct(Group $group)
{
$this->group = $group;
}
/**
* @return \Source\Entities\Group
*/
public function getGroup(): Group
{
return $this->group;
}
}
......@@ -49,12 +49,13 @@ class CreateUser implements CreateUserUseCase
null,
);
$response = new ResponseModel($user);
if (!$this->usersRepository->create($user)) {
if (!($user = $this->usersRepository->create($user))) {
throw new EntityExistsException();
}
$response = new ResponseModel($user);
$presenter->present($response);
}
}
<?php
namespace Tests\Feature\Api\Groups;
use Source\Entities\Group;
use Source\Gateways\Groups\GroupsRepository;
use Source\Exceptions\EntityNotFoundException;
use Illuminate\Foundation\Testing\TestResponse;
use Source\Gateways\Groups\InMemoryGroupsRepository;
use Tests\Feature\AuthenticatesWithApplicationTestCase;
class CreateGroupApiTest extends AuthenticatesWithApplicationTestCase
{
protected TestResponse $response;
protected InMemoryGroupsRepository $groups;
public function setUp(): void
{
parent::setUp();
$this->groups = $this->app->make(GroupsRepository::class);
}
/**
* @param array $data
*/
protected function handleTest(array $data): void
{
$this->response = $this->postJson('/groups',
array_merge(
['api_token' => $this->authToken],
$data
)
);
}
/**
* @test
*/
public function it_denies_unauthorized(): void
{
$this->handleTest([]);
$this->response->assertStatus(401);
}
/**
* @test
* @throws \Source\Exceptions\EntityNotFoundException
*/
public function it_protects_the_route(): void
{
$this->authorize(false);
$this->handleTest([]);
$this->response->assertStatus(403);
}
public function invalidGroupProvider(): array
{
$group = new Group(0, 'title', 'desc');
return [
// Missing one of the required
[
[
'title' => $group->getTitle(),
],
['description' => ['The description field is required.']],
],
[
[
'description' => $group->getDescription(),
],
['title' => ['The title field is required.']],
],
];
}
/**
* @test
* @param array $data
* @param array $message
* @dataProvider invalidGroupProvider
* @throws EntityNotFoundException
*/
public function it_tests_the_validation_rules(array $data, array $message): void
{
$this->authenticate();
$this->handleTest($data);
$this->response->assertJsonFragment($message);
}
/**
* @test
* @throws EntityNotFoundException
*/
public function it_creates_a_group(): void
{
$this->authenticate();
$this->handleTest([
'title' => 'title',
'description' => 'desc',
]);
$this->response->assertStatus(200);
$this->response->assertJsonFragment([
'title' => 'title',
'description' => 'desc',
]);
$this->assertCount(1, $this->groups->all());
}
}
<?php
namespace Tests\Unit\Source\UseCases\Groups\CreateGroup;
use Source\UseCases\Groups\CreateGroup\Presenter;