API methods to handle teams and users (#1384)

This commit is contained in:
Kevin Papst
2020-01-28 22:28:31 +01:00
committed by GitHub
parent a31e027c54
commit 836a530b86
24 changed files with 1525 additions and 107 deletions

View File

@@ -21,8 +21,11 @@ nelmio_api_doc:
- { alias: TimesheetMeta, type: App\Entity\TimesheetMeta, groups: [Default, Timesheet] }
- { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] }
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
- { alias: UserCreateForm, type: App\Form\API\UserApiCreateForm, groups: [Default, Entity, User, User_Entity] }
- { alias: UserEditForm, type: App\Form\API\UserApiEditForm, groups: [Default, Entity, User, User_Entity] }
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User, User_Entity] }
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
- { alias: TeamEditForm, type: App\Form\API\TeamApiEditForm, groups: [Default, Entity, Team] }
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team, Team_Entity] }
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }

View File

@@ -1,6 +1,6 @@
App\Entity\Customer:
exclusion_policy: All
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixedRate, hourlyRate, color, budget, timeBudget, metaFields]
custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, email, homepage, timezone, fixedRate, hourlyRate, color, budget, timeBudget, metaFields, teams]
properties:
id:
include: true
@@ -63,6 +63,9 @@ App\Entity\Customer:
include: true
metaFields:
exclude: true
teams:
include: true
groups: [Customer]
virtual_properties:
getMetaFields:
serialized_name: metaFields

View File

@@ -1,6 +1,6 @@
App\Entity\Project:
exclusion_policy: All
custom_accessor_order: [id, name, comment, visible, orderNumber, orderDate, customer, start, end, fixedRate, hourlyRate, color, budget, timeBudget, metaFields, parentTitle]
custom_accessor_order: [id, name, comment, visible, orderNumber, orderDate, customer, start, end, fixedRate, hourlyRate, color, budget, timeBudget, metaFields, parentTitle, teams]
properties:
id:
include: true
@@ -41,6 +41,9 @@ App\Entity\Project:
include: true
metaFields:
exclude: true
teams:
include: true
groups: [Project]
virtual_properties:
parentTitle:
serialized_name: parentTitle

View File

@@ -1,6 +1,6 @@
App\Entity\Team:
exclusion_policy: All
custom_accessor_order: [id, name, teamlead, users]
custom_accessor_order: [id, name, teamlead, users, customers, projects]
properties:
id:
include: true
@@ -13,6 +13,8 @@ App\Entity\Team:
include: true
groups: [Team_Entity]
customers:
exclude: true
include: true
groups: [Team_Entity]
projects:
exclude: true
include: true
groups: [Team_Entity]

View File

@@ -11,8 +11,15 @@ declare(strict_types=1);
namespace App\API;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Entity\User;
use App\Form\API\TeamApiEditForm;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\TeamRepository;
use App\Repository\UserRepository;
use FOS\RestBundle\Controller\Annotations\RouteResource;
use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
@@ -20,23 +27,25 @@ use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* @RouteResource("Team")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
class TeamController extends BaseApiController
final class TeamController extends BaseApiController
{
/**
* @var TeamRepository
*/
protected $repository;
private $repository;
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
private $viewHandler;
public function __construct(ViewHandlerInterface $viewHandler, TeamRepository $repository)
{
@@ -135,4 +144,481 @@ class TeamController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Creates a new team
*
* @SWG\Post(
* description="Creates a new team and returns it afterwards",
* @SWG\Response(
* response=200,
* description="Returns the new created team",
* @SWG\Schema(ref="#/definitions/TeamEntity",),
* )
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/TeamEditForm")
* )
*
* @Security("is_granted('create_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postAction(Request $request): Response
{
$team = new Team();
$team->setTeamLead($this->getUser());
$form = $this->createForm(TeamApiEditForm::class, $team);
$form->submit($request->request->all());
$team->addUser($team->getTeamLead());
if ($form->isValid()) {
$this->repository->saveTeam($team);
$view = new View($team, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
$view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Update an existing team
*
* @SWG\Patch(
* description="Update an existing team, you can pass all or just a subset of all attributes (passing users will replace all existing ones)",
* @SWG\Response(
* response=200,
* description="Returns the updated team",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/TeamEditForm")
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="Team ID to update",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function patchAction(Request $request, int $id): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException();
}
$form = $this->createForm(TeamApiEditForm::class, $team);
$form->setData($team);
$form->submit($request->request->all(), false);
$team->addUser($team->getTeamLead());
if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Add a new member to a team
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new user to a team. The user must not be deactivated.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team which will receive the new member",
* required=true,
* )
* @SWG\Parameter(
* name="userId",
* in="path",
* type="integer",
* description="The team member to add (User ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postMemberAction(int $id, int $userId, UserRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var User $user */
$user = $repository->find($userId);
if (null === $user) {
throw new NotFoundException('User not found');
}
if (!$user->isEnabled()) {
throw new BadRequestHttpException('Cannot add disabled user to team');
}
if ($user->isInTeam($team)) {
throw new BadRequestHttpException('User is already member of the team');
}
$team->addUser($user);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Removes a member from the team
*
* @SWG\Delete(
* @SWG\Response(
* response=200,
* description="Removes a user from the team. The teamlead cannot be removed.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team from which the member will be removed",
* required=true,
* )
* @SWG\Parameter(
* name="userId",
* in="path",
* type="integer",
* description="The team member to remove (User ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteMemberAction(int $id, int $userId, UserRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var User $user */
$user = $repository->find($userId);
if (null === $user) {
throw new NotFoundException('User not found');
}
if (!$user->isInTeam($team)) {
throw new BadRequestHttpException('User is not a member of the team');
}
if ($team->isTeamlead($user)) {
throw new BadRequestHttpException('Cannot remove teamlead');
}
$team->removeUser($user);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Grant the team access to a customer
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new customer to a team. The customer must not be invisible.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team that is granted access",
* required=true,
* )
* @SWG\Parameter(
* name="customerId",
* in="path",
* type="integer",
* description="The customer to grant acecess to (Customer ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postCustomerAction(int $id, int $customerId, CustomerRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var Customer $customer */
$customer = $repository->find($customerId);
if (null === $customer) {
throw new NotFoundException('Customer not found');
}
if (!$customer->isVisible()) {
throw new BadRequestHttpException('Cannot grant access to an invisible customer');
}
if ($team->hasCustomer($customer)) {
throw new BadRequestHttpException('Team has already access to customer');
}
$team->addCustomer($customer);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Revokes access for a customer from a team
*
* @SWG\Delete(
* @SWG\Response(
* response=200,
* description="Removes a customer from the team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team whose permission will be revoked",
* required=true,
* )
* @SWG\Parameter(
* name="customerId",
* in="path",
* type="integer",
* description="The customer to remove (Customer ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteCustomerAction(int $id, int $customerId, CustomerRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var Customer $customer */
$customer = $repository->find($customerId);
if (null === $customer) {
throw new NotFoundException('Customer not found');
}
if (!$team->hasCustomer($customer)) {
throw new BadRequestHttpException('Customer is not assigned to the team');
}
$team->removeCustomer($customer);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Grant the team access to a project
*
* @SWG\Post(
* @SWG\Response(
* response=200,
* description="Adds a new project to a team. The project must not be invisible.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team that is granted access",
* required=true,
* )
* @SWG\Parameter(
* name="projectId",
* in="path",
* type="integer",
* description="The project to grant acecess to (Project ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postProjectAction(int $id, int $projectId, ProjectRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var Project $project */
$project = $repository->find($projectId);
if (null === $project) {
throw new NotFoundException('Project not found');
}
if (!$project->isVisible()) {
throw new BadRequestHttpException('Cannot grant access to an invisible project');
}
if ($team->hasProject($project)) {
throw new BadRequestHttpException('Team has already access to project');
}
$team->addProject($project);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Revokes access for a project from a team
*
* @SWG\Delete(
* @SWG\Response(
* response=200,
* description="Removes a project from the team.",
* @SWG\Schema(ref="#/definitions/TeamEntity")
* )
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="The team whose permission will be revoked",
* required=true,
* )
* @SWG\Parameter(
* name="projectId",
* in="path",
* type="integer",
* description="The project to remove (Project ID)",
* required=true,
* )
*
* @Security("is_granted('edit_team')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function deleteProjectAction(int $id, int $projectId, ProjectRepository $repository): Response
{
$team = $this->repository->find($id);
if (null === $team) {
throw new NotFoundException('Team not found');
}
/** @var Project $project */
$project = $repository->find($projectId);
if (null === $project) {
throw new NotFoundException('Project not found');
}
if (!$team->hasProject($project)) {
throw new BadRequestHttpException('Project is not assigned to the team');
}
$team->removeProject($project);
$this->repository->saveTeam($team);
$view = new View($team, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'Team_Entity']);
return $this->viewHandler->handle($view);
}
}

View File

@@ -11,6 +11,10 @@ declare(strict_types=1);
namespace App\API;
use App\Configuration\FormConfiguration;
use App\Entity\User;
use App\Form\API\UserApiCreateForm;
use App\Form\API\UserApiEditForm;
use App\Repository\Query\UserQuery;
use App\Repository\UserRepository;
use App\Utils\SearchTerm;
@@ -22,34 +26,45 @@ use FOS\RestBundle\View\ViewHandlerInterface;
use Nelmio\ApiDocBundle\Annotation\Security as ApiSecurity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
/**
* @RouteResource("User")
*
* @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
*/
class UserController extends BaseApiController
final class UserController extends BaseApiController
{
/**
* @var UserRepository
*/
protected $repository;
private $repository;
/**
* @var ViewHandlerInterface
*/
protected $viewHandler;
private $viewHandler;
/**
* @var UserPasswordEncoderInterface
*/
private $encoder;
/**
* @var FormConfiguration
*/
private $configuration;
/**
* @param ViewHandlerInterface $viewHandler
* @param UserRepository $repository
*/
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository)
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository, UserPasswordEncoderInterface $encoder, FormConfiguration $config)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->encoder = $encoder;
$this->configuration = $config;
}
/**
@@ -159,4 +174,125 @@ class UserController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Creates a new user
*
* @SWG\Post(
* description="Creates a new user and returns it afterwards",
* @SWG\Response(
* response=200,
* description="Returns the new created user",
* @SWG\Schema(ref="#/definitions/UserEntity",),
* )
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/UserCreateForm")
* )
*
* @Security("is_granted('create_user')")
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function postAction(Request $request): Response
{
$user = new User();
$user->setEnabled(true);
$user->setRoles([User::DEFAULT_ROLE]);
$user->setTimezone($this->configuration->getUserDefaultTimezone());
$user->setLanguage($this->configuration->getUserDefaultLanguage());
$form = $this->createForm(UserApiCreateForm::class, $user, [
'include_roles' => $this->isGranted('roles', $user),
'include_active_flag' => true,
'include_preferences' => $this->isGranted('preferences', $user),
]);
$form->submit($request->request->all());
if ($form->isValid()) {
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$this->repository->saveUser($user);
$view = new View($user, 200);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
return $this->viewHandler->handle($view);
}
$view = new View($form);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
return $this->viewHandler->handle($view);
}
/**
* Update an existing user
*
* @SWG\Patch(
* description="Update an existing user, you can pass all or just a subset of all attributes (passing roles will replace all existing ones)",
* @SWG\Response(
* response=200,
* description="Returns the updated user",
* @SWG\Schema(ref="#/definitions/UserEntity")
* )
* )
* @SWG\Parameter(
* name="body",
* in="body",
* required=true,
* @SWG\Schema(ref="#/definitions/UserEditForm")
* )
* @SWG\Parameter(
* name="id",
* in="path",
* type="integer",
* description="User ID to update",
* required=true,
* )
*
* @ApiSecurity(name="apiUser")
* @ApiSecurity(name="apiToken")
*/
public function patchAction(Request $request, int $id): Response
{
$user = $this->repository->getUserById($id);
if (null === $user) {
throw new NotFoundException();
}
if (!$this->isGranted('edit', $user)) {
throw new AccessDeniedHttpException('Not allowed to edit user');
}
$form = $this->createForm(UserApiEditForm::class, $user, [
'include_roles' => $this->isGranted('roles', $user),
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
'include_preferences' => $this->isGranted('preferences', $user),
]);
$form->setData($user);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
$view = new View($form, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
return $this->viewHandler->handle($view);
}
$this->repository->saveUser($user);
$view = new View($user, Response::HTTP_OK);
$view->getContext()->setGroups(['Default', 'Entity', 'User', 'User_Entity']);
return $this->viewHandler->handle($view);
}
}

View File

@@ -20,7 +20,6 @@ use App\Form\UserRolesType;
use App\Form\UserTeamsType;
use App\Repository\TeamRepository;
use App\Repository\TimesheetRepository;
use App\Voter\UserVoter;
use Doctrine\Common\Collections\ArrayCollection;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -281,23 +280,23 @@ class ProfileController extends AbstractController
): Response {
$forms = [];
if ($this->isGranted(UserVoter::EDIT, $user)) {
if ($this->isGranted('edit', $user)) {
$editForm = $editForm ?: $this->createEditForm($user);
$forms['settings'] = $editForm->createView();
}
if ($this->isGranted(UserVoter::PASSWORD, $user)) {
if ($this->isGranted('password', $user)) {
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
$forms['password'] = $pwdForm->createView();
}
if ($this->isGranted(UserVoter::API_TOKEN, $user)) {
if ($this->isGranted('api-token', $user)) {
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
$forms['api-token'] = $apiTokenForm->createView();
}
if ($this->isGranted(UserVoter::TEAMS, $user) && $this->teams->count([]) > 0) {
if ($this->isGranted('teams', $user) && $this->teams->count([]) > 0) {
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
$forms['teams'] = $teamsForm->createView();
}
if ($this->isGranted(UserVoter::ROLES, $user)) {
if ($this->isGranted('roles', $user)) {
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
$forms['roles'] = $rolesForm->createView();
}
@@ -329,7 +328,8 @@ class ProfileController extends AbstractController
[
'action' => $this->generateUrl('user_profile_edit', ['username' => $user->getUsername()]),
'method' => 'POST',
'include_active_flag' => ($user->getId() !== $this->getUser()->getId())
'include_active_flag' => ($user->getId() !== $this->getUser()->getId()),
'include_preferences' => false,
]
);
}

View File

@@ -9,6 +9,7 @@
namespace App\Controller;
use App\Configuration\FormConfiguration;
use App\Entity\User;
use App\Event\UserPreferenceDisplayEvent;
use App\Form\Toolbar\UserToolbarForm;
@@ -31,20 +32,20 @@ use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
* @Route(path="/admin/user")
* @Security("is_granted('view_user')")
*/
class UserController extends AbstractController
final class UserController extends AbstractController
{
/**
* @var UserPasswordEncoderInterface
*/
protected $encoder;
private $encoder;
/**
* @var UserRepository
*/
protected $repository;
private $repository;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
private $dispatcher;
public function __construct(UserPasswordEncoderInterface $encoder, UserRepository $repository, EventDispatcherInterface $dispatcher)
{
@@ -93,23 +94,31 @@ class UserController extends AbstractController
]);
}
private function createNewDefaultUser(FormConfiguration $config): User
{
$user = new User();
$user->setEnabled(true);
$user->setRoles([User::DEFAULT_ROLE]);
$user->setTimezone($config->getUserDefaultTimezone());
$user->setLanguage($config->getUserDefaultLanguage());
return $user;
}
/**
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
* @Security("is_granted('create_user')")
*/
public function createAction(Request $request): Response
public function createAction(Request $request, FormConfiguration $config): Response
{
$user = new User();
$user->setEnabled(true);
$editForm = $this->createEditForm($user);
$user = $this->createNewDefaultUser($config);
$editForm = $this->getCreateUserForm($user);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$password = $this->encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$user->setEnabled(true);
$user->setRoles([User::DEFAULT_ROLE]);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
@@ -121,8 +130,12 @@ class UserController extends AbstractController
return $this->redirectToRoute('user_profile_edit', ['username' => $user->getUsername()]);
}
$user = new User();
$editForm = $this->createEditForm($user);
$firstUser = $user;
$user = $this->createNewDefaultUser($config);
$user->setLanguage($firstUser->getLanguage());
$user->setTimezone($firstUser->getTimezone());
$editForm = $this->getCreateUserForm($user);
$editForm->get('create_more')->setData(true);
}
@@ -187,12 +200,14 @@ class UserController extends AbstractController
]);
}
private function createEditForm(User $user): FormInterface
private function getCreateUserForm(User $user): FormInterface
{
return $this->createForm(UserCreateType::class, $user, [
'action' => $this->generateUrl('admin_user_create'),
'method' => 'POST',
'include_active_flag' => true
'include_active_flag' => true,
'include_preferences' => $this->isGranted('preferences', $user),
'include_add_more' => true,
]);
}
}

View File

@@ -144,6 +144,11 @@ class Team
return $this->users;
}
public function hasCustomer(Customer $customer): bool
{
return $this->customers->contains($customer);
}
public function addCustomer(Customer $customer)
{
if ($this->customers->contains($customer)) {
@@ -172,6 +177,11 @@ class Team
return $this->customers;
}
public function hasProject(Project $project): bool
{
return $this->projects->contains($project);
}
public function addProject(Project $project)
{
if ($this->projects->contains($project)) {

View File

@@ -266,6 +266,27 @@ class User extends BaseUser implements UserInterface
return $this->getPreferenceValue(UserPreference::TIMEZONE, date_default_timezone_get());
}
public function getLanguage(): string
{
return $this->getLocale();
}
public function setLanguage(?string $language)
{
if ($language === null) {
$language = User::DEFAULT_LANGUAGE;
}
$this->setPreferenceValue(UserPreference::LOCALE, $language);
}
public function setTimezone(?string $timezone)
{
if ($timezone === null) {
$timezone = date_default_timezone_get();
}
$this->setPreferenceValue(UserPreference::TIMEZONE, $timezone);
}
/**
* @param string $name
* @param mixed $default

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\API;
use App\Form\TeamEditForm;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TeamApiEditForm extends TeamEditForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'expand_users' => false,
'csrf_protection' => false,
]);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\API;
use App\Form\Type\UserRoleType;
use App\Form\UserCreateType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserApiCreateForm extends UserCreateType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->remove('plainPassword');
$builder->add('plainPassword', PasswordType::class, [
'required' => true,
'label' => 'label.password',
'documentation' => [
'type' => 'string',
'description' => 'Plain text password',
],
]);
if ($options['include_roles']) {
$builder->add('roles', UserRoleType::class, [
'label' => 'label.roles',
'required' => false,
'multiple' => true,
'expanded' => false,
]);
}
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
'include_roles' => true,
'include_add_more' => false,
]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Form\API;
use App\Form\Type\UserRoleType;
use App\Form\UserEditType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserApiEditForm extends UserEditType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
if ($options['include_roles']) {
$builder->add('roles', UserRoleType::class, [
'label' => 'label.roles',
'required' => false,
'multiple' => true,
'expanded' => false,
]);
}
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
'include_roles' => true,
]);
}
}

View File

@@ -29,15 +29,25 @@ class TeamEditForm extends AbstractType
'attr' => [
'autofocus' => 'autofocus'
],
// documentation is for NelmioApiDocBundle
'documentation' => [
'type' => 'string',
'description' => 'Name of the new team',
],
])
->add('teamlead', UserType::class, [
'label' => 'label.teamlead',
'multiple' => false,
'expanded' => false,
// documentation is for NelmioApiDocBundle
'documentation' => [
'type' => 'integer',
'description' => 'User ID for the teamlead',
],
])
->add('users', UserType::class, [
'multiple' => true,
'expanded' => true,
'expanded' => $options['expand_users'],
'by_reference' => false,
])
;
@@ -52,6 +62,7 @@ class TeamEditForm extends AbstractType
'data_class' => Team::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'expand_users' => true,
'csrf_token_id' => 'admin_team_edit',
'attr' => [
'data-form-event' => 'kimai.teamUpdate'

View File

@@ -9,7 +9,6 @@
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
@@ -43,20 +42,21 @@ class UserCreateType extends UserEditType
parent::buildForm($builder, $options);
$builder->add('create_more', CheckboxType::class, [
'label' => 'label.create_more',
'required' => false,
'mapped' => false,
]);
if ($options['include_add_more'] === true) {
$builder->add('create_more', CheckboxType::class, [
'label' => 'label.create_more',
'required' => false,
'mapped' => false,
]);
}
}
/**
* {@inheritdoc}
*/
public function __configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'class' => User::class,
'include_add_more' => false,
]);
}
}

View File

@@ -11,10 +11,12 @@ namespace App\Form;
use App\Entity\User;
use App\Form\Type\AvatarType;
use App\Form\Type\LanguageType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -45,6 +47,16 @@ class UserEditType extends AbstractType
])
;
if ($options['include_preferences']) {
$builder->add('language', LanguageType::class, [
'required' => true,
]);
$builder->add('timezone', TimezoneType::class, [
'required' => true,
]);
}
if ($options['include_active_flag']) {
$builder
->add('enabled', YesNoType::class, [
@@ -64,7 +76,8 @@ class UserEditType extends AbstractType
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'edit_user_profile',
'include_active_flag' => false,
'include_active_flag' => true,
'include_preferences' => true,
]);
}
}

View File

@@ -17,6 +17,7 @@ use App\Repository\Query\BaseQuery;
use App\Repository\Query\UserFormTypeQuery;
use App\Repository\Query\UserQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
@@ -32,7 +33,19 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
}
/**
* Used to fetch the currently logged-in user.
* @param User $user
* @throws ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function saveUser(User $user)
{
$entityManager = $this->getEntityManager();
$entityManager->persist($user);
$entityManager->flush();
}
/**
* Used to fetch a user by its ID.
*
* @param int $id
* @return null|User

View File

@@ -15,17 +15,13 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class TeamVoter extends AbstractVoter
{
public const VIEW = 'view';
public const EDIT = 'edit';
public const DELETE = 'delete';
/**
* support rules based on the given $subject (here: Team)
*/
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::DELETE,
'view',
'edit',
'delete',
];
/**

View File

@@ -17,26 +17,16 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
*/
class UserVoter extends AbstractVoter
{
public const VIEW = 'view';
public const EDIT = 'edit';
public const DELETE = 'delete';
public const PASSWORD = 'password';
public const ROLES = 'roles';
public const TEAMS = 'teams';
public const PREFERENCES = 'preferences';
public const API_TOKEN = 'api-token';
public const HOURLY_RATE = 'hourly-rate';
public const ALLOWED_ATTRIBUTES = [
self::VIEW,
self::EDIT,
self::ROLES,
self::TEAMS,
self::PASSWORD,
self::DELETE,
self::PREFERENCES,
self::API_TOKEN,
self::HOURLY_RATE,
'view',
'edit',
'roles',
'teams',
'password',
'delete',
'preferences',
'api-token',
'hourly-rate',
];
/**
@@ -71,39 +61,21 @@ class UserVoter extends AbstractVoter
return false;
}
$permission = '';
switch ($attribute) {
// special case for the UserController
case self::DELETE:
if ($subject->getId() === $user->getId()) {
return false;
}
return $this->hasRolePermission($user, 'delete_user');
case self::VIEW:
case self::EDIT:
case self::PREFERENCES:
case self::PASSWORD:
case self::API_TOKEN:
case self::ROLES:
case self::TEAMS:
case self::HOURLY_RATE:
$permission .= $attribute;
break;
default:
if ($attribute === 'delete') {
if ($subject->getId() === $user->getId()) {
return false;
}
return $this->hasRolePermission($user, 'delete_user');
}
$permission .= '_';
$permission = $attribute;
// extend me for "team" support later on
if ($subject->getId() == $user->getId()) {
$permission .= 'own';
if ($subject->getId() === $user->getId()) {
$permission .= '_own';
} else {
$permission .= 'other';
$permission .= '_other';
}
$permission .= '_profile';

View File

@@ -230,7 +230,7 @@ class CustomerControllerTest extends APIControllerBaseTest
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name', 'visible', 'hourlyRate', 'fixedRate', 'color', 'metaFields'
'id', 'name', 'visible', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'teams'
];
if ($full) {

View File

@@ -277,7 +277,7 @@ class ProjectControllerTest extends APIControllerBaseTest
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
'id', 'name', 'visible', 'customer', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'parentTitle', 'start', 'end'
'id', 'name', 'visible', 'customer', 'hourlyRate', 'fixedRate', 'color', 'metaFields', 'parentTitle', 'start', 'end', 'teams'
];
if ($full) {

View File

@@ -9,6 +9,8 @@
namespace App\Tests\API;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\User;
use App\Tests\DataFixtures\TeamFixtures;
use Symfony\Component\HttpFoundation\Response;
@@ -42,7 +44,7 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(2, count($result));
self::assertEquals(2, count($result));
$this->assertStructure($result[0], false);
}
@@ -66,6 +68,64 @@ class TeamControllerTest extends APIControllerBaseTest
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/teams/255', []);
}
public function testPostAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
$this->assertNotEmpty($result['id']);
}
public function testPostActionWithInvalidUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertFalse($response->isSuccessful());
self::assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
$json = json_decode($response->getContent(), true);
self::assertEquals('Access denied.', $json['message']);
}
public function testPatchAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 2,
'users' => [1, 5, 4]
];
$this->request($client, '/api/teams/' . $result['id'], 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
$this->assertNotEmpty($result['id']);
self::assertCount(4, $result['users']);
}
public function testDeleteAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
@@ -79,12 +139,443 @@ class TeamControllerTest extends APIControllerBaseTest
$this->request($client, '/api/teams/' . $id, 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
self::assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
$this->assertEmpty($client->getResponse()->getContent());
$this->assertEntityNotFound(User::ROLE_ADMIN, '/api/teams/' . $id);
}
public function testPostMemberAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(1, $result['users']);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
self::assertCount(2, $result['users']);
}
public function testPostMemberActionErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
// team not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/999/members/999', 'POST');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team not found', $json['message']);
// user not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/999', 'POST');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('User not found', $json['message']);
// add user
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/5', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
// cannot add existing member
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/5', 'POST');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('User is already member of the team', $json['message']);
// cannot add disabled user
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/3', 'POST');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Cannot add disabled user to team', $json['message']);
}
public function testDeleteMemberAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2, 4, 5]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(4, $result['users']);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
self::assertCount(3, $result['users']);
}
public function testDeleteMemberActionErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2, 4, 5]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
// team not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/999/members/999', 'DELETE');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team not found', $json['message']);
// user not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/999', 'DELETE');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('User not found', $json['message']);
// remove user
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
// cannot remove non-member
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/2', 'DELETE');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('User is not a member of the team', $json['message']);
// cannot remove teamlead
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/members/1', 'DELETE');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Cannot remove teamlead', $json['message']);
}
public function testPostCustomerAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(0, $result['customers']);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
self::assertCount(1, $result['customers']);
self::assertEquals(1, $result['customers'][0]['id']);
}
public function testPostCustomerActionErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
// team not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/999/customers/999', 'POST');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team not found', $json['message']);
// customer not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/999', 'POST');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Customer not found', $json['message']);
// add customer
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(1, $result['customers']);
// cannot add existing customer
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team has already access to customer', $json['message']);
$customer = new Customer();
$customer->setName('foooo');
$customer->setVisible(false);
$customer->setCountry('DE');
$customer->setTimezone('Europe/Berlin');
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$em->persist($customer);
$em->flush();
// cannot add invisible customer
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/' . $customer->getId(), 'POST');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Cannot grant access to an invisible customer', $json['message']);
}
public function testDeleteCustomerAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2, 4, 5]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(0, $result['customers']);
// add customer
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(1, $result['customers']);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
self::assertCount(0, $result['customers']);
}
public function testDeleteCustomerActionErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2, 4, 5]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
// team not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/999/customers/999', 'DELETE');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team not found', $json['message']);
// customer not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/999', 'DELETE');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Customer not found', $json['message']);
// cannot remove customer
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/customers/1', 'DELETE');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Customer is not assigned to the team', $json['message']);
}
public function testPostProjectAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(0, $result['projects']);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
self::assertCount(1, $result['projects']);
self::assertEquals(1, $result['projects'][0]['id']);
}
public function testPostProjectActionErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
// team not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/999/projects/999', 'POST');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team not found', $json['message']);
// project not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/999', 'POST');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Project not found', $json['message']);
// add project
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(1, $result['projects']);
// cannot add existing project
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team has already access to project', $json['message']);
$customer = new Customer();
$customer->setName('foooo');
$customer->setVisible(false);
$customer->setCountry('DE');
$customer->setTimezone('Europe/Berlin');
$project = new Project();
$project->setName('foooo');
$project->setVisible(false);
$project->setCustomer($customer);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$em->persist($customer);
$em->persist($project);
$em->flush();
// cannot add invisible project
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/' . $project->getId(), 'POST');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Cannot grant access to an invisible project', $json['message']);
}
public function testDeleteProjectAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2, 4, 5]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(0, $result['projects']);
// add project
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'POST');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
self::assertCount(1, $result['projects']);
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'DELETE');
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
self::assertCount(0, $result['projects']);
}
public function testDeleteProjectActionErrors()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'name' => 'foo',
'teamlead' => 1,
'users' => [2, 4, 5]
];
$this->request($client, '/api/teams', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
// team not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/999/projects/999', 'DELETE');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Team not found', $json['message']);
// project not found
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/999', 'DELETE');
self::assertEquals(Response::HTTP_NOT_FOUND, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Project not found', $json['message']);
// cannot remove project
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$this->request($client, '/api/teams/' . $result['id'] . '/projects/1', 'DELETE');
self::assertEquals(Response::HTTP_BAD_REQUEST, $client->getResponse()->getStatusCode());
$json = json_decode($client->getResponse()->getContent(), true);
self::assertEquals('Project is not assigned to the team', $json['message']);
}
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = [
@@ -93,7 +584,7 @@ class TeamControllerTest extends APIControllerBaseTest
if ($full) {
$expectedKeys = array_merge($expectedKeys, [
'teamlead', 'users'
'teamlead', 'users', 'customers', 'projects'
]);
}
@@ -101,6 +592,6 @@ class TeamControllerTest extends APIControllerBaseTest
sort($actual);
sort($expectedKeys);
$this->assertEquals($expectedKeys, $actual, 'Team structure does not match');
self::assertEquals($expectedKeys, $actual, 'Team structure does not match');
}
}

View File

@@ -10,6 +10,7 @@
namespace App\Tests\API;
use App\Entity\User;
use Symfony\Component\HttpFoundation\Response;
/**
* @group integration
@@ -113,6 +114,106 @@ class UserControllerTest extends APIControllerBaseTest
$this->assertStructure($result);
}
public function testPostAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'username' => 'foo',
'email' => 'foo@example.com',
'avatar' => 'test123',
'title' => 'asdfghjkl',
'plainPassword' => 'foo@example.com',
'enabled' => true,
'language' => 'ru',
'timezone' => 'Europe/Paris',
'roles' => [
'ROLE_TEAMLEAD',
'ROLE_ADMIN'
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
$this->assertNotEmpty($result['id']);
self::assertEquals('foo', $result['username']);
self::assertEquals('test123', $result['avatar']);
self::assertEquals('asdfghjkl', $result['title']);
self::assertTrue($result['enabled']);
self::assertEquals('ru', $result['language']);
self::assertEquals('Europe/Paris', $result['timezone']);
self::assertEquals(['ROLE_TEAMLEAD', 'ROLE_ADMIN'], $result['roles']);
}
public function testPostActionWithInvalidUser()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$data = [
'username' => 'foo',
'email' => 'foo@example.com',
'plainPassword' => 'foo@example.com',
'enabled' => true,
'language' => 'ru',
'timezone' => 'Europe/Paris',
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$response = $client->getResponse();
$this->assertFalse($response->isSuccessful());
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
$json = json_decode($response->getContent(), true);
$this->assertEquals('Access denied.', $json['message']);
}
public function testPatchAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'username' => 'foo',
'email' => 'foo@example.com',
'avatar' => 'test123',
'title' => 'asdfghjkl',
'plainPassword' => 'foo@example.com',
'enabled' => true,
'language' => 'ru',
'timezone' => 'Europe/Paris',
'roles' => [
'ROLE_TEAMLEAD',
'ROLE_ADMIN'
],
];
$this->request($client, '/api/users', 'POST', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$data = [
'avatar' => 'test321',
'title' => 'qwertzui',
'enabled' => false,
'language' => 'it',
'timezone' => 'America/New_York',
'roles' => [
'ROLE_TEAMLEAD',
],
];
$this->request($client, '/api/users/' . $result['id'], 'PATCH', [], json_encode($data));
$this->assertTrue($client->getResponse()->isSuccessful());
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertStructure($result);
$this->assertNotEmpty($result['id']);
self::assertEquals('foo', $result['username']);
self::assertEquals('test321', $result['avatar']);
self::assertEquals('qwertzui', $result['title']);
self::assertFalse($result['enabled']);
self::assertEquals('it', $result['language']);
self::assertEquals('America/New_York', $result['timezone']);
self::assertEquals(['ROLE_TEAMLEAD'], $result['roles']);
}
protected function assertStructure(array $result, $full = true)
{
$expectedKeys = ['id', 'username', 'enabled', 'alias'];

View File

@@ -57,8 +57,10 @@ class TeamTest extends TestCase
self::assertEmpty($customer->getTeams());
$sut = new Team();
self::assertFalse($sut->hasCustomer($customer));
$sut->addCustomer($customer);
self::assertEquals(1, $sut->getCustomers()->count());
self::assertTrue($sut->hasCustomer($customer));
$actual = $sut->getCustomers()[0];
self::assertSame($actual, $customer);
self::assertSame($sut, $customer->getTeams()[0]);
@@ -75,8 +77,10 @@ class TeamTest extends TestCase
self::assertEmpty($project->getTeams());
$sut = new Team();
self::assertFalse($sut->hasProject($project));
$sut->addProject($project);
self::assertEquals(1, $sut->getProjects()->count());
self::assertTrue($sut->hasProject($project));
$actual = $sut->getProjects()[0];
self::assertSame($actual, $project);
self::assertSame($sut, $project->getTeams()[0]);