diff --git a/config/packages/fos_rest.yaml b/config/packages/fos_rest.yaml index 015f043f..aedd722b 100644 --- a/config/packages/fos_rest.yaml +++ b/config/packages/fos_rest.yaml @@ -1,5 +1,5 @@ fos_rest: -# disable_csrf_role: ROLE_API +# disable_csrf_role: IS_AUTHENTICATED_ANONYMOUSLY # access_denied_listener: # enabled: false # service: null @@ -18,8 +18,8 @@ fos_rest: default_format: json include_format: true # prefix_methods: true -# body_converter: -# enabled: false + body_converter: + enabled: true # validate: false # validation_errors_argument: validationErrors # service: @@ -46,8 +46,8 @@ fos_rest: # name: ~ # templating_formats: # name: ~ -# view_response_listener: -# enabled: true + view_response_listener: + enabled: true # force: true # service: null failed_validation: 400 @@ -67,16 +67,16 @@ fos_rest: messages: 'Symfony\Component\Routing\Exception\ResourceNotFoundException': true 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException': true -# body_listener: -# enabled: true + body_listener: + enabled: true # service: null # default_format: null # throw_exception_on_unsupported_content_type: false # decoders: # name: ~ -# array_normalizer: -# service: null -# forms: false + array_normalizer: + service: null + forms: true format_listener: enabled: true # service: null diff --git a/config/packages/jms_serializer.yaml b/config/packages/jms_serializer.yaml index 33abf699..1fe047cf 100644 --- a/config/packages/jms_serializer.yaml +++ b/config/packages/jms_serializer.yaml @@ -1,4 +1,7 @@ jms_serializer: + handlers: + datetime: + default_format: "Y-m-d H:i" visitors: xml: format_output: '%kernel.debug%' diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml index 45535377..4db4bc66 100644 --- a/config/packages/nelmio_api_doc.yaml +++ b/config/packages/nelmio_api_doc.yaml @@ -1,16 +1,35 @@ nelmio_api_doc: models: use_jms: true + names: + - { alias: CustomerEntity, type: App\Entity\Customer, groups: [Default, Entity, Customer] } + - { alias: CustomerCollection, type: App\Entity\Customer, groups: [Default, Collection, Customer] } + - { alias: ProjectEntity, type: App\Entity\Project, groups: [Default, Entity, Project] } + - { alias: ProjectCollection, type: App\Entity\Project, groups: [Default, Collection, Project] } + - { alias: ActivityEntity, type: App\Entity\Activity, groups: [Default, Entity, Activity] } + - { alias: ActivityCollection, type: App\Entity\Activity, groups: [Default, Collection, Activity] } + - { alias: TimesheetFormEntity, type: App\Form\TimesheetEditForm, groups: [Default, Entity, Timesheet] } + - { alias: TimesheetEntity, type: App\Entity\Timesheet, groups: [Default, Entity, Timesheet] } + - { alias: TimesheetCollection, type: App\Entity\Timesheet, groups: [Default, Collection, Timesheet] } + - { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] } + - { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] } areas: path_patterns: - ^/api(?!/doc) documentation: - #host: '{{hostname}}' +# host: '{{hostname}}' schemes: [http, https] info: title: Kimai 2 - API Docs - description: REST API description for the Kimai 2 time-tracking software - version: 0.1 + description: REST API description for the Kimai 2 time-tracking software. Do not rely on the example values/models, they are currently generated wrong. Please fetch the request and response structure from the API itself. + version: 0.2 +# parameters: +# hostname: +# name: hostname +# in: url +# description: the hostname where your api runs +# required: true +# type: string securityDefinitions: X-AUTH-USER: type: apiKey diff --git a/config/serializer/App/Entity.Activity.yml b/config/serializer/App/Entity.Activity.yml index 59aeb70e..642717f5 100644 --- a/config/serializer/App/Entity.Activity.yml +++ b/config/serializer/App/Entity.Activity.yml @@ -1,18 +1,32 @@ App\Entity\Activity: exclusion_policy: All - custom_accessor_order: [id, name, comment, visible, project_id] + custom_accessor_order: [id, name, comment, visible, project, fixedRate, hourlyRate] properties: id: include: true + groups: [Default] name: include: true + groups: [Default] comment: include: true + groups: [Entity] visible: include: true + groups: [Default] + fixedRate: + include: true + groups: [Default] + hourlyRate: + include: true + groups: [Default] + project: + include: false + exclude: true + groups: [Default] virtual_properties: - getProjectId: - name: project_id - serialized_name: project_id + getProject: + serialized_name: project exp: "object.getProject() === null ? null : object.getProject().getId()" type: integer + groups: [Default] diff --git a/config/serializer/App/Entity.Customer.yml b/config/serializer/App/Entity.Customer.yml index 8e33de48..e19960a1 100644 --- a/config/serializer/App/Entity.Customer.yml +++ b/config/serializer/App/Entity.Customer.yml @@ -1,35 +1,58 @@ App\Entity\Customer: exclusion_policy: All + custom_accessor_order: [id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone, fixedRate, hourlyRate] properties: id: include: true + groups: [Default] name: include: true + groups: [Default] number: include: true + groups: [Entity] comment: include: true + groups: [Entity] visible: include: true + groups: [Default] company: include: true + groups: [Entity] contact: include: true + groups: [Entity] address: include: true + groups: [Entity] country: include: true + groups: [Entity] currency: include: true + groups: [Entity] phone: include: true + groups: [Entity] fax: include: true + groups: [Entity] mobile: include: true + groups: [Entity] mail: include: true + groups: [Entity] homepage: include: true + groups: [Entity] timezone: include: true + groups: [Entity] + fixedRate: + include: true + groups: [Default] + hourlyRate: + include: true + groups: [Default] diff --git a/config/serializer/App/Entity.Project.yml b/config/serializer/App/Entity.Project.yml index d7446203..2f36716f 100644 --- a/config/serializer/App/Entity.Project.yml +++ b/config/serializer/App/Entity.Project.yml @@ -1,22 +1,38 @@ App\Entity\Project: exclusion_policy: All - custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer_id] + custom_accessor_order: [id, name, comment, visible, budget, orderNumber, customer, fixedRate, hourlyRate] properties: id: include: true + groups: [Default] name: include: true + groups: [Default] comment: include: true + groups: [Entity] visible: include: true + groups: [Default] budget: include: true + groups: [Entity] orderNumber: include: true + groups: [Entity] + fixedRate: + include: true + groups: [Entity] + hourlyRate: + include: true + groups: [Entity] + customer: + include: false + exclude: true + groups: [Default] virtual_properties: - getCustomerId: - name: customer_id - serialized_name: customer_id + getCustomer: + serialized_name: customer exp: "object.getCustomer() === null ? null : object.getCustomer().getId()" type: integer + groups: [Default] diff --git a/config/serializer/App/Entity.Timesheet.yml b/config/serializer/App/Entity.Timesheet.yml new file mode 100644 index 00000000..9515e2fa --- /dev/null +++ b/config/serializer/App/Entity.Timesheet.yml @@ -0,0 +1,56 @@ +App\Entity\Timesheet: + exclusion_policy: All + custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate] + properties: + id: + include: true + groups: [Default] + begin: + include: true + groups: [Default] + end: + include: true + groups: [Default] + duration: + include: true + groups: [Default] + description: + include: true + groups: [Entity] + rate: + include: true + groups: [Default] + fixedRate: + include: true + groups: [Entity] + hourlyRate: + include: true + groups: [Entity] + activity: + include: false + exclude: true + groups: [Default] + project: + include: false + exclude: true + groups: [Default] + user: + include: false + exclude: true + groups: [Default] + virtual_properties: + getActivity: + serialized_name: activity + exp: "object.getActivity() === null ? null : object.getActivity().getId()" + type: integer + groups: [Default] + getProject: + serialized_name: project + exp: "object.getProject() === null ? null : object.getProject().getId()" + type: integer + groups: [Default] + getUser: + serialized_name: user + exp: "object.getUser().getId()" + type: integer + groups: [Default] diff --git a/config/serializer/App/Entity.User.yml b/config/serializer/App/Entity.User.yml index 29ccd21d..4fe7a477 100644 --- a/config/serializer/App/Entity.User.yml +++ b/config/serializer/App/Entity.User.yml @@ -3,9 +3,13 @@ App\Entity\User: properties: id: include: true + groups: [Default] alias: include: true + groups: [Default] title: include: true + groups: [Entity] avatar: include: true + groups: [Entity] diff --git a/config/serializer/FOS/UserBundle/Model.User.yml b/config/serializer/FOS/UserBundle/Model.User.yml index b10eeb2b..7382a506 100644 --- a/config/serializer/FOS/UserBundle/Model.User.yml +++ b/config/serializer/FOS/UserBundle/Model.User.yml @@ -1,6 +1,15 @@ FOS\UserBundle\Model\User: exclusion_policy: NONE properties: + enabled: + include: true + groups: [Default] + groups: + include: true + groups: [Entity] + roles: + include: true + groups: [Entity] email: exclude: true emailCanonical: diff --git a/src/API/ActivityController.php b/src/API/ActivityController.php index a32370ae..10c51ed6 100644 --- a/src/API/ActivityController.php +++ b/src/API/ActivityController.php @@ -19,10 +19,8 @@ use FOS\RestBundle\Controller\Annotations\RouteResource; use FOS\RestBundle\Request\ParamFetcherInterface; use FOS\RestBundle\View\View; use FOS\RestBundle\View\ViewHandlerInterface; -use Nelmio\ApiDocBundle\Annotation as API; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Swagger\Annotations as SWG; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; /** @@ -30,7 +28,7 @@ use Symfony\Component\HttpFoundation\Response; * * @Security("is_granted('ROLE_USER')") */ -class ActivityController extends Controller +class ActivityController extends BaseApiController { /** * @var ActivityRepository @@ -54,13 +52,13 @@ class ActivityController extends Controller /** * @SWG\Response( - * response=200, - * description="Returns the collection of all existing activities", - * @SWG\Schema(ref=@API\Model(type=Activity::class)), + * response=200, + * description="Returns the collection of all existing activities", + * @SWG\Schema(ref="#/definitions/ActivityCollection"), * ) * @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter activities. If none is provided, only global activities will be returned.") - * @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities") - * @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Pass true to fetch only global activities") + * @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter activities (1=visible, 2=hidden, 3=both)") + * @Rest\QueryParam(name="globals", requirements="true", strict=true, nullable=true, description="Pass 'true' as string to fetch only global activities") * * @return Response */ @@ -84,15 +82,16 @@ class ActivityController extends Controller $data = $this->repository->findByQuery($query); $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Collection', 'Activity']); return $this->viewHandler->handle($view); } /** * @SWG\Response( - * response=200, - * description="Returns one activity entity", - * @SWG\Schema(ref=@API\Model(type=Activity::class)), + * response=200, + * description="Returns one activity entity", + * @SWG\Schema(ref="#/definitions/ActivityEntity"), * ) * * @param int $id @@ -105,6 +104,7 @@ class ActivityController extends Controller throw new NotFoundException(); } $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Entity', 'Activity']); return $this->viewHandler->handle($view); } diff --git a/src/API/BaseApiController.php b/src/API/BaseApiController.php new file mode 100644 index 00000000..719fb180 --- /dev/null +++ b/src/API/BaseApiController.php @@ -0,0 +1,18 @@ +repository->findAll(); + $query = new CustomerQuery(); + $query->setResultType(CustomerQuery::RESULT_TYPE_OBJECTS); + + if (null !== ($visible = $paramFetcher->get('visible'))) { + $query->setVisibility($visible); + } + + $data = $this->repository->findByQuery($query); $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Collection', 'Customer']); return $this->viewHandler->handle($view); } /** * @SWG\Response( - * response=200, - * description="Returns one customer entity", - * @SWG\Schema(ref=@Model(type=Customer::class)), + * response=200, + * description="Returns one customer entity", + * @SWG\Schema(ref="#/definitions/CustomerEntity"), * ) * * @param int $id @@ -83,6 +93,7 @@ class CustomerController extends Controller throw new NotFoundException(); } $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Entity', 'Customer']); return $this->viewHandler->handle($view); } diff --git a/src/API/HealthcheckController.php b/src/API/HealthcheckController.php index 9cb97aa5..178bc19a 100644 --- a/src/API/HealthcheckController.php +++ b/src/API/HealthcheckController.php @@ -37,6 +37,7 @@ class HealthcheckController extends Controller * @SWG\Response( * response=200, * description="A simple route that returns a 'pong', which you can use for testing the API", + * examples={"{'message': 'pong'}"} * ) * * @Rest\Get(path="/ping") diff --git a/src/API/ProjectController.php b/src/API/ProjectController.php index b126ade5..6259e837 100644 --- a/src/API/ProjectController.php +++ b/src/API/ProjectController.php @@ -19,10 +19,8 @@ use FOS\RestBundle\Controller\Annotations\RouteResource; use FOS\RestBundle\Request\ParamFetcherInterface; use FOS\RestBundle\View\View; use FOS\RestBundle\View\ViewHandlerInterface; -use Nelmio\ApiDocBundle\Annotation\Model; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Swagger\Annotations as SWG; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; /** @@ -30,7 +28,7 @@ use Symfony\Component\HttpFoundation\Response; * * @Security("is_granted('ROLE_USER')") */ -class ProjectController extends Controller +class ProjectController extends BaseApiController { /** * @var ProjectRepository @@ -54,12 +52,12 @@ class ProjectController extends Controller /** * @SWG\Response( - * response=200, - * description="Returns the collection of all existing projects", - * @SWG\Schema(ref=@Model(type=Project::class)), + * response=200, + * description="Returns the collection of all existing projects", + * @SWG\Schema(ref="#/definitions/ProjectCollection"), * ) * @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter projects") - * @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects") + * @Rest\QueryParam(name="visible", requirements="\d+", strict=true, nullable=true, description="Visibility status to filter projects (1=visible, 2=hidden, 3=both)") * * @param ParamFetcherInterface $paramFetcher * @return Response @@ -79,15 +77,16 @@ class ProjectController extends Controller $data = $this->repository->findByQuery($query); $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Collection', 'Project']); return $this->viewHandler->handle($view); } /** * @SWG\Response( - * response=200, - * description="Returns one project entity", - * @SWG\Schema(ref=@Model(type=Project::class)), + * response=200, + * description="Returns one project entity", + * @SWG\Schema(ref="#/definitions/ProjectEntity"), * ) * * @param int $id @@ -100,6 +99,7 @@ class ProjectController extends Controller throw new NotFoundException(); } $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Entity', 'Project']); return $this->viewHandler->handle($view); } diff --git a/src/API/TimesheetController.php b/src/API/TimesheetController.php new file mode 100644 index 00000000..0f710af4 --- /dev/null +++ b/src/API/TimesheetController.php @@ -0,0 +1,191 @@ +viewHandler = $viewHandler; + $this->repository = $repository; + } + + /** + * @SWG\Response( + * response=200, + * description="Returns the collection of all existing timesheets for the user", + * @SWG\Schema(ref="#/definitions/TimesheetCollection"), + * ) + * @Rest\QueryParam(name="customer", requirements="\d+", strict=true, nullable=true, description="Customer ID to filter timesheets") + * @Rest\QueryParam(name="project", requirements="\d+", strict=true, nullable=true, description="Project ID to filter timesheets") + * @Rest\QueryParam(name="activity", requirements="\d+", strict=true, nullable=true, description="Activity ID to filter timesheets") + * @Rest\QueryParam(name="page", requirements="\d+", strict=true, nullable=true, description="The page to display, renders a 404 if not found (default: 1)") + * @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries for each page (default: 25)") + * @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order (allowed values: 'ASC', 'DESC')") + * @Rest\QueryParam(name="orderBy", requirements="id|begin|end|rate", strict=true, nullable=true, description="The field by which results will be ordered (allowed values: 'id', 'begin', 'end', 'rate')") + * + * @return Response + */ + public function cgetAction(ParamFetcherInterface $paramFetcher) + { + $query = new TimesheetQuery(); + $query->setUser($this->getUser()); + $query->setResultType(TimesheetQuery::RESULT_TYPE_PAGER); + + if (null !== ($customer = $paramFetcher->get('customer'))) { + $query->setCustomer($customer); + } + + if (null !== ($project = $paramFetcher->get('project'))) { + $query->setProject($project); + } + + if (null !== ($activity = $paramFetcher->get('activity'))) { + $query->setActivity($activity); + } + + if (null !== ($page = $paramFetcher->get('page'))) { + $query->setPage($page); + } + + if (null !== ($size = $paramFetcher->get('size'))) { + $query->setPageSize($size); + } + + if (null !== ($order = $paramFetcher->get('order'))) { + $query->setOrder($order); + } + + if (null !== ($orderBy = $paramFetcher->get('orderBy'))) { + $query->setOrderBy($orderBy); + } + + /** @var Pagerfanta $data */ + $data = $this->repository->findByQuery($query); + $data = (array) $data->getCurrentPageResults(); + + $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Collection', 'Timesheet']); + + return $this->viewHandler->handle($view); + } + + /** + * @SWG\Response( + * response=200, + * description="Returns one timesheet entity", + * @SWG\Schema(ref="#/definitions/TimesheetEntity") + * ) + * + * @param int $id + * @return Response + */ + public function getAction($id) + { + $data = $this->repository->find($id); + if (null === $data) { + throw new NotFoundException(); + } + $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); + + return $this->viewHandler->handle($view); + } + + /** + * @SWG\Post( + * description="Creates a new timesheet entry and returns it afterwards", + * @SWG\Schema(ref="#/definitions/TimesheetFormEntity"), + * @SWG\Response( + * response=200, + * description="Returns the new created timesheet entry", + * @SWG\Schema(ref="#/definitions/TimesheetEntity"), + * ) + * ) + * + * @param Request $request + * @return Response + */ + public function postAction(Request $request) + { + $timesheet = new Timesheet(); + $timesheet->setUser($this->getUser()); + $timesheet->setBegin(new \DateTime()); + + $form = $this->createForm(TimesheetEditForm::class, $timesheet, [ + 'csrf_protection' => false, + 'method' => 'POST', + 'duration_only' => false, + ]); + + $form->setData($timesheet); + $form->submit($request->request->all()); + + if ($form->isValid()) { + if (null !== $timesheet->getId()) { + return new Response('This method does not support updates', Response::HTTP_BAD_REQUEST); + } + + if (!$this->isGranted('start', $timesheet)) { + return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST); + } + + $entityManager = $this->getDoctrine()->getManager(); + $entityManager->persist($timesheet); + $entityManager->flush(); + + $view = new View($timesheet, 200); + $view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); + + return $this->viewHandler->handle($view); + } + + $view = new View($form); + $view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']); + + return $this->viewHandler->handle($view); + } +} diff --git a/src/API/UserController.php b/src/API/UserController.php index 7c787957..c8342ee0 100644 --- a/src/API/UserController.php +++ b/src/API/UserController.php @@ -16,10 +16,8 @@ use App\Repository\UserRepository; use FOS\RestBundle\Controller\Annotations\RouteResource; use FOS\RestBundle\View\View; use FOS\RestBundle\View\ViewHandlerInterface; -use Nelmio\ApiDocBundle\Annotation\Model; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Swagger\Annotations as SWG; -use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Response; /** @@ -28,7 +26,7 @@ use Symfony\Component\HttpFoundation\Response; * @Security("is_granted('ROLE_SUPER_ADMIN')") * @Security("is_granted('IS_AUTHENTICATED_FULLY')") */ -class UserController extends Controller +class UserController extends BaseApiController { /** * @var UserRepository @@ -52,9 +50,9 @@ class UserController extends Controller /** * @SWG\Response( - * response=200, - * description="Returns the collection of all registered users", - * @SWG\Schema(ref=@Model(type=User::class)), + * response=200, + * description="Returns the collection of all registered users", + * @SWG\Schema(ref="#/definitions/UserCollection"), * ) * * @return Response @@ -63,15 +61,16 @@ class UserController extends Controller { $data = $this->repository->findAll(); $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Collection', 'User']); return $this->viewHandler->handle($view); } /** * @SWG\Response( - * response=200, - * description="Return one user entity", - * @SWG\Schema(ref=@Model(type=User::class)), + * response=200, + * description="Return one user entity", + * @SWG\Schema(ref="#/definitions/UserEntity"), * ) * * @param int $id @@ -84,6 +83,7 @@ class UserController extends Controller throw new NotFoundException(); } $view = new View($data, 200); + $view->getContext()->setGroups(['Default', 'Entity', 'User']); return $this->viewHandler->handle($view); } diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 7e3c199c..5440c080 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -354,28 +354,58 @@ class Timesheet */ public function validate(ExecutionContextInterface $context, $payload) { - if (null === $this->getActivity()) { + if (null === ($activity = $this->getActivity())) { $context->buildViolation('A timesheet must have an activity.') ->atPath('activity') ->setTranslationDomain('validators') ->addViolation(); } - if (null === $this->getProject()) { + if (null === ($project = $this->getProject())) { $context->buildViolation('A timesheet must have a project.') ->atPath('project') ->setTranslationDomain('validators') ->addViolation(); } - if (null !== $this->getActivity() && null !== $this->getProject() && null !== $this->getActivity()->getProject() && $this->getActivity()->getProject() !== $this->getProject()) { - $context->buildViolation('Project mismatch, project specific activity and timesheet project are different.') - ->atPath('project') + if (null !== $activity && null !== $project) { + if (null !== $activity->getProject() && $activity->getProject() !== $project) { + $context->buildViolation('Project mismatch, project specific activity and timesheet project are different.') + ->atPath('project') + ->setTranslationDomain('validators') + ->addViolation(); + } + + if ($activity->getVisible() === false) { + $context->buildViolation('Cannot start a disabled activity.') + ->atPath('activity') + ->setTranslationDomain('validators') + ->addViolation(); + } + + if ($project->getVisible() === false) { + $context->buildViolation('Cannot start a disabled project.') + ->atPath('project') + ->setTranslationDomain('validators') + ->addViolation(); + } + + if ($project->getCustomer()->getVisible() === false) { + $context->buildViolation('Cannot start a disabled customer.') + ->atPath('customer') + ->setTranslationDomain('validators') + ->addViolation(); + } + } + + if (null === $this->getBegin() && null !== $this->getEnd()) { + $context->buildViolation('You must submit a begin date before an end date is added.') + ->atPath('begin') ->setTranslationDomain('validators') ->addViolation(); } - if (null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) { + if (null !== $this->getBegin() && null !== $this->getEnd() && $this->getEnd()->getTimestamp() < $this->getBegin()->getTimestamp()) { $context->buildViolation('End date must not be earlier then start date.') ->atPath('end') ->setTranslationDomain('validators') diff --git a/src/Form/TimesheetEditForm.php b/src/Form/TimesheetEditForm.php index 6e3797e0..c04e7195 100644 --- a/src/Form/TimesheetEditForm.php +++ b/src/Form/TimesheetEditForm.php @@ -37,18 +37,27 @@ class TimesheetEditForm extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - /** @var Timesheet $entry */ - $entry = $options['data']; + $activity = null; + $project = null; + $customer = null; + $end = null; - $activity = $entry->getActivity(); - $project = $entry->getProject(); - $customer = null === $entry->getProject() ? null : $entry->getProject()->getCustomer(); + if (isset($options['data'])) { + /** @var Timesheet $entry */ + $entry = $options['data']; - if (null === $project && null !== $activity) { - $project = $activity->getProject(); + $activity = $entry->getActivity(); + $project = $entry->getProject(); + $customer = null === $entry->getProject() ? null : $entry->getProject()->getCustomer(); + + if (null === $project && null !== $activity) { + $project = $activity->getProject(); + } + + $end = $entry->getEnd(); } - if (null === $entry->getEnd() || !$options['duration_only']) { + if (null === $end || !$options['duration_only']) { $builder->add('begin', DateTimeType::class, [ 'label' => 'label.begin', 'widget' => 'single_text', @@ -75,6 +84,11 @@ class TimesheetEditForm extends AbstractType $builder ->add('customer', CustomerType::class, [ + // documentation is for NelmioApiDocBundle + 'documentation' => [ + 'type' => 'integer', + 'description' => 'Customer ID', + ], 'label' => 'label.customer', 'query_builder' => function (CustomerRepository $repo) use ($customer) { return $repo->builderForEntityType($customer); @@ -88,6 +102,11 @@ class TimesheetEditForm extends AbstractType ], ]) ->add('project', ProjectType::class, [ + // documentation is for NelmioApiDocBundle + 'documentation' => [ + 'type' => 'integer', + 'description' => 'Project ID', + ], 'required' => true, 'placeholder' => '', 'label' => 'label.project', @@ -100,6 +119,11 @@ class TimesheetEditForm extends AbstractType ], ]) ->add('activity', ActivityType::class, [ + // documentation is for NelmioApiDocBundle + 'documentation' => [ + 'type' => 'integer', + 'description' => 'Activity ID', + ], 'label' => 'label.activity', 'query_builder' => function (ActivityRepository $repo) use ($activity) { return $repo->builderForEntityType($activity); diff --git a/src/Invoice/Renderer/RendererTrait.php b/src/Invoice/Renderer/RendererTrait.php index f7f1ccc8..9ea675b5 100644 --- a/src/Invoice/Renderer/RendererTrait.php +++ b/src/Invoice/Renderer/RendererTrait.php @@ -149,10 +149,10 @@ trait RendererTrait 'entry.duration' => $timesheet->getDuration(), 'entry.duration_minutes' => number_format($timesheet->getDuration() / 60), 'entry.begin' => $this->getFormattedDateTime($begin), - 'entry.begin_time' => date("H:i", $begin->getTimestamp()), + 'entry.begin_time' => date('H:i', $begin->getTimestamp()), 'entry.begin_timestamp' => $begin->getTimestamp(), 'entry.end' => $this->getFormattedDateTime($end), - 'entry.end_time' => date("H:i", $end->getTimestamp()), + 'entry.end_time' => date('H:i', $end->getTimestamp()), 'entry.end_timestamp' => $end->getTimestamp(), 'entry.date' => $this->getFormattedDateTime($begin), 'entry.user_id' => $user->getId(), diff --git a/src/Twig/Extensions.php b/src/Twig/Extensions.php index c6226e04..6e593589 100644 --- a/src/Twig/Extensions.php +++ b/src/Twig/Extensions.php @@ -132,6 +132,7 @@ class Extensions extends \Twig_Extension public function isDatatableConfigured(string $dataTable, string $size) { $cookie = $this->getVisibilityCookieName($dataTable, $size); + return $this->requestStack->getCurrentRequest()->cookies->has($cookie); } diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php index 4278db84..58eee53e 100644 --- a/tests/API/APIControllerBaseTest.php +++ b/tests/API/APIControllerBaseTest.php @@ -139,11 +139,14 @@ abstract class APIControllerBaseTest extends ControllerBaseTest * @param string $url * @param string $method * @param array $parameters + * @param string $content * @return Crawler */ - protected function request(Client $client, string $url, $method = 'GET', array $parameters = []) + protected function request(Client $client, string $url, $method = 'GET', array $parameters = [], string $content = null) { - return $client->request($method, $this->createUrl($url), $parameters, [], ['HTTP_CONTENT_TYPE' => 'application/json']); + $server = ['HTTP_CONTENT_TYPE' => 'application/json', 'CONTENT_TYPE' => 'application/json']; + + return $client->request($method, $this->createUrl($url), $parameters, [], $server, $content); } /** @@ -167,4 +170,23 @@ abstract class APIControllerBaseTest extends ControllerBaseTest json_decode($client->getResponse()->getContent(), true) ); } + + /** + * @param Response $response + * @param string[] $failedFields + */ + protected function assertApiCallValidationError(Response $response, array $failedFields) + { + $this->assertFalse($response->isSuccessful()); + $result = json_decode($response->getContent(), true); + + $this->assertArrayHasKey('errors', $result); + $this->assertArrayHasKey('children', $result['errors']); + $data = $result['errors']['children']; + + foreach ($failedFields as $fieldName) { + $this->assertArrayHasKey($fieldName, $data); + $this->assertArrayHasKey('errors', $data[$fieldName]); + } + } } diff --git a/tests/API/ActivityControllerTest.php b/tests/API/ActivityControllerTest.php index 451a0c53..63f9533e 100644 --- a/tests/API/ActivityControllerTest.php +++ b/tests/API/ActivityControllerTest.php @@ -75,7 +75,7 @@ class ActivityControllerTest extends APIControllerBaseTest $hasProject = $expected[$i][0]; $this->assertStructure($activity, $hasProject); if ($hasProject) { - $this->assertEquals($expected[$i][0], $activity['project_id']); + $this->assertEquals($expected[$i][0], $activity['project']); } } } @@ -100,7 +100,10 @@ class ActivityControllerTest extends APIControllerBaseTest $result = json_decode($client->getResponse()->getContent(), true); $this->assertInternalType('array', $result); - $this->assertStructure($result, false); + + $expectedKeys = ['id', 'name', 'comment', 'visible']; + $actual = array_keys($result); + $this->assertEquals($expectedKeys, $actual); } public function testNotFound() @@ -108,19 +111,18 @@ class ActivityControllerTest extends APIControllerBaseTest $this->assertEntityNotFound(User::ROLE_USER, '/api/activities/2'); } - protected function assertStructure(array $result, $project = true) + protected function assertStructure(array $result, $full = true) { - $expectedKeys = [ - 'id', 'name', 'comment', 'visible' - ]; + $expectedKeys = ['id', 'name', 'visible']; - if ($project) { - $expectedKeys[] = 'project_id'; + if ($full) { + $expectedKeys = ['id', 'name', 'visible', 'project']; } $actual = array_keys($result); + sort($actual); + sort($expectedKeys); - $this->assertEquals(count($expectedKeys), count($actual), 'Activity entity has different amount of keys: ' . $result['id']); $this->assertEquals($expectedKeys, $actual, 'Activity structure does not match'); } } diff --git a/tests/API/CustomerControllerTest.php b/tests/API/CustomerControllerTest.php index 5e4384a9..5080cdbd 100644 --- a/tests/API/CustomerControllerTest.php +++ b/tests/API/CustomerControllerTest.php @@ -31,7 +31,7 @@ class CustomerControllerTest extends APIControllerBaseTest $this->assertInternalType('array', $result); $this->assertNotEmpty($result); $this->assertEquals(1, count($result)); - $this->assertStructure($result[0]); + $this->assertStructure($result[0], false); } public function testGetEntity() @@ -41,7 +41,7 @@ class CustomerControllerTest extends APIControllerBaseTest $result = json_decode($client->getResponse()->getContent(), true); $this->assertInternalType('array', $result); - $this->assertStructure($result); + $this->assertStructure($result, true); } public function testNotFound() @@ -49,16 +49,21 @@ class CustomerControllerTest extends APIControllerBaseTest $this->assertEntityNotFound(User::ROLE_USER, '/api/customers/2'); } - protected function assertStructure(array $result) + protected function assertStructure(array $result, $full = true) { - $expectedKeys = [ - 'id', 'name', 'number', 'comment', 'visible', 'company', 'contact', 'address', 'country', 'currency', - 'phone', 'fax', 'mobile', 'mail', 'timezone' - ]; + $expectedKeys = ['id', 'name', 'visible']; + + if ($full) { + $expectedKeys = [ + 'id', 'name', 'number', 'comment', 'visible', 'company', 'contact', 'address', 'country', 'currency', + 'phone', 'fax', 'mobile', 'mail', 'timezone' + ]; + } $actual = array_keys($result); + sort($actual); + sort($expectedKeys); - $this->assertEquals(count($expectedKeys), count($actual), 'Customer entity has different amount of keys'); $this->assertEquals($expectedKeys, $actual, 'Customer structure does not match'); } } diff --git a/tests/API/ProjectControllerTest.php b/tests/API/ProjectControllerTest.php index 51cd8867..06548271 100644 --- a/tests/API/ProjectControllerTest.php +++ b/tests/API/ProjectControllerTest.php @@ -35,7 +35,7 @@ class ProjectControllerTest extends APIControllerBaseTest $this->assertInternalType('array', $result); $this->assertNotEmpty($result); $this->assertEquals(1, count($result)); - $this->assertStructure($result[0]); + $this->assertStructure($result[0], false); } protected function loadProjectTestData(Client $client) @@ -87,8 +87,8 @@ class ProjectControllerTest extends APIControllerBaseTest for ($i = 0; $i < count($expected); $i++) { $project = $result[$i]; $compare = $expected[$i]; - $this->assertStructure($project, $compare[0]); - $this->assertEquals($compare[1], $project['customer_id']); + $this->assertStructure($project, false); + $this->assertEquals($compare[1], $project['customer']); } } @@ -119,21 +119,22 @@ class ProjectControllerTest extends APIControllerBaseTest $this->assertEntityNotFound(User::ROLE_USER, '/api/projects/2'); } - protected function assertStructure(array $result, $complete = true) + protected function assertStructure(array $result, $full = true) { $expectedKeys = [ - 'id', 'name', 'comment', 'visible', 'budget', 'order_number', 'customer_id' + 'id', 'name', 'comment', 'visible', 'budget', 'order_number', 'customer' ]; - if (!$complete) { + if (!$full) { $expectedKeys = [ - 'id', 'name', 'visible', 'budget', 'customer_id' + 'id', 'name', 'visible', 'customer' ]; } $actual = array_keys($result); + sort($actual); + sort($expectedKeys); - $this->assertEquals(count($expectedKeys), count($actual), 'Project entity has different amount of keys'); $this->assertEquals($expectedKeys, $actual, 'Project structure does not match'); } } diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php new file mode 100644 index 00000000..d525d2e4 --- /dev/null +++ b/tests/API/TimesheetControllerTest.php @@ -0,0 +1,187 @@ +getClientForAuthenticatedUser(User::ROLE_USER); + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + + $fixture = new TimesheetFixtures(); + $fixture + ->setFixedRate(true) + ->setHourlyRate(true) + ->setAmount(10) + ->setUser($this->getUserByRole($em, User::ROLE_USER)) + ->setStartDate(new \DateTime('-10 days')) + ; + $this->importFixture($em, $fixture); + } + + public function testIsSecure() + { + $this->assertUrlIsSecured('/api/timesheets'); + } + + public function testGetCollection() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertNotEmpty($result); + $this->assertEquals(10, count($result)); + $this->assertDefaultStructure($result[0], false); + } + + public function testGetEntity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $this->assertAccessIsGranted($client, '/api/timesheets/1'); + $result = json_decode($client->getResponse()->getContent(), true); + + $this->assertInternalType('array', $result); + $this->assertDefaultStructure($result); + } + + public function testPostAction() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $data = [ + 'activity' => 1, + 'project' => 1, + 'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m'), + 'end' => (new \DateTime())->format('Y-m-d H:m'), + 'description' => 'foo', + 'fixedRate' => 2016, + 'hourlyRate' => 127 + ]; + $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $this->assertTrue($client->getResponse()->isSuccessful()); + + $result = json_decode($client->getResponse()->getContent(), true); + $this->assertInternalType('array', $result); + $this->assertDefaultStructure($result); + $this->assertNotEmpty($result['id']); + $this->assertEquals(28800, $result['duration']); + $this->assertEquals(2016, $result['rate']); + } + + // check for project, as this is a required field. It will not be included in the select, as it is + // already filtered within the repository due to the hidden customer + public function testPostActionWithInvisibleProject() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $customer = (new Customer())->setName('foo-bar-1')->setVisible(false)->setCountry('DE')->setTimezone('Euopre/Berlin'); + $em->persist($customer); + $project = (new Project())->setName('foo-bar-2')->setVisible(true)->setCustomer($customer); + $em->persist($project); + $activity = (new Activity())->setName('foo-bar-3')->setVisible(true); + $em->persist($activity); + $em->flush(); + + $data = [ + 'activity' => $activity->getId(), + 'project' => $project->getId(), + 'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m'), + 'end' => (new \DateTime())->format('Y-m-d H:m'), + 'description' => 'foo', + 'fixedRate' => 2016, + 'hourlyRate' => 127 + ]; + $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $this->assertApiCallValidationError($client->getResponse(), ['project']); + } + + // check for activity, as this is a required field. It will not be included in the select, as it is + // already filtered within the repository due to the hidden flag + public function testPostActionWithInvisibleActivity() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + + $em = $client->getContainer()->get('doctrine.orm.entity_manager'); + $customer = (new Customer())->setName('foo-bar-1')->setVisible(true)->setCountry('DE')->setTimezone('Euopre/Berlin'); + $em->persist($customer); + $project = (new Project())->setName('foo-bar-2')->setVisible(true)->setCustomer($customer); + $em->persist($project); + $activity = (new Activity())->setName('foo-bar-3')->setVisible(false); + $em->persist($activity); + $em->flush(); + + $data = [ + 'activity' => $activity->getId(), + 'project' => $project->getId(), + 'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m'), + 'end' => (new \DateTime())->format('Y-m-d H:m'), + 'description' => 'foo', + 'fixedRate' => 2016, + 'hourlyRate' => 127 + ]; + $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $this->assertApiCallValidationError($client->getResponse(), ['activity']); + } + + public function testPostActionWithIdIsNotAllowed() + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_USER); + $data = [ + 'id' => 1, + 'activity' => 1, + 'project' => 1, + 'begin' => (new \DateTime('- 8 hours'))->format('Y-m-d H:m'), + 'end' => (new \DateTime())->format('Y-m-d H:m'), + 'description' => 'foo', + 'fixedRate' => 2016, + 'hourlyRate' => 127 + ]; + $this->request($client, '/api/timesheets', 'POST', [], json_encode($data)); + $this->assertFalse($client->getResponse()->isSuccessful()); + $this->assertEquals(400, $client->getResponse()->getStatusCode()); + } + + public function testNotFound() + { + $this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/20'); + } + + protected function assertDefaultStructure(array $result, $full = true) + { + $expectedKeys = [ + 'id', 'begin', 'end', 'duration', 'rate', 'activity', 'project', 'user' + ]; + + if ($full) { + $expectedKeys = array_merge($expectedKeys, [ + 'description', 'fixed_rate', 'hourly_rate' + ]); + } + + $actual = array_keys($result); + sort($actual); + sort($expectedKeys); + + $this->assertEquals($expectedKeys, $actual, 'Timesheet structure does not match'); + } +} diff --git a/tests/API/UserControllerTest.php b/tests/API/UserControllerTest.php index 72899c4c..429069e9 100644 --- a/tests/API/UserControllerTest.php +++ b/tests/API/UserControllerTest.php @@ -32,7 +32,7 @@ class UserControllerTest extends APIControllerBaseTest $this->assertInternalType('array', $result); $this->assertNotEmpty($result); $this->assertEquals(6, count($result)); - $this->assertStructure($result[0]); + $this->assertStructure($result[0], false); } public function testGetEntity() @@ -50,15 +50,18 @@ class UserControllerTest extends APIControllerBaseTest $this->assertEntityNotFound(User::ROLE_SUPER_ADMIN, '/api/users/99'); } - protected function assertStructure(array $result) + protected function assertStructure(array $result, $full = true) { - $expectedKeys = [ - 'id', 'username', 'enabled', 'roles', 'alias', 'title', 'avatar' - ]; + $expectedKeys = ['id', 'username', 'enabled', 'alias']; + + if ($full) { + $expectedKeys = ['id', 'username', 'enabled', 'roles', 'alias', 'title', 'avatar']; + } $actual = array_keys($result); + sort($actual); + sort($expectedKeys); - $this->assertEquals(count($expectedKeys), count($actual), 'User entity has different amount of keys'); $this->assertEquals($expectedKeys, $actual, 'User structure does not match'); } } diff --git a/tests/Calendar/TimesheetEntityTest.php b/tests/Calendar/TimesheetEntityTest.php new file mode 100644 index 00000000..16326b26 --- /dev/null +++ b/tests/Calendar/TimesheetEntityTest.php @@ -0,0 +1,80 @@ +setName('activity'); + + $customer = new Customer(); + $customer->setName('customer'); + + $project = new Project(); + $project->setName('project'); + $project->setCustomer($customer); + + $timesheet = new Timesheet(); + $timesheet->setActivity($activity); + $timesheet->setProject($project); + + $sut = new TimesheetEntity($timesheet); + + $this->assertEquals('customer', $sut->getCustomer()); + $this->assertEquals('project', $sut->getProject()); + $this->assertEquals('activity', $sut->getTitle()); + + $sut->setId(13); + $this->assertEquals(13, $sut->getId()); + + $date = new \DateTime('-13 hours'); + $sut->setStart($date); + $this->assertEquals($date, $sut->getStart()); + + $date = new \DateTime('-3 hours'); + $sut->setEnd($date); + $this->assertEquals($date, $sut->getEnd()); + + $sut->setTitle('sdfsdf'); + $this->assertEquals('sdfsdf', $sut->getTitle()); + + $sut->setCustomer('aaaaaaaa'); + $this->assertEquals('aaaaaaaa', $sut->getCustomer()); + + $sut->setProject('bbbbbbbbbb'); + $this->assertEquals('bbbbbbbbbb', $sut->getProject()); + + $sut->setActivity('cccccccc'); + $this->assertEquals('cccccccc', $sut->getActivity()); + + $this->assertEquals('#f39c12', $sut->getBorderColor()); + $sut->setBorderColor('#cccccc'); + $this->assertEquals('#cccccc', $sut->getBorderColor()); + + $this->assertEquals('#f39c12', $sut->getBackgroundColor()); + $sut->setBackgroundColor('#ffffff'); + $this->assertEquals('#ffffff', $sut->getBackgroundColor()); + + $sut->setDescription('foo-bar'); + $this->assertEquals('foo-bar', $sut->getDescription()); + } +} diff --git a/tests/Controller/ControllerBaseTest.php b/tests/Controller/ControllerBaseTest.php index 8a800dea..39127f8b 100644 --- a/tests/Controller/ControllerBaseTest.php +++ b/tests/Controller/ControllerBaseTest.php @@ -82,11 +82,12 @@ abstract class ControllerBaseTest extends WebTestCase * @param string $url * @param string $method * @param array $parameters + * @param string $content * @return \Symfony\Component\DomCrawler\Crawler */ - protected function request(Client $client, string $url, $method = 'GET', array $parameters = []) + protected function request(Client $client, string $url, $method = 'GET', array $parameters = [], string $content = null) { - return $client->request($method, $this->createUrl($url), $parameters); + return $client->request($method, $this->createUrl($url), $parameters, [], [], $content); } /** diff --git a/tests/Controller/HelpControllerTest.php b/tests/Controller/HelpControllerTest.php index 9d10ea0f..0a582e08 100644 --- a/tests/Controller/HelpControllerTest.php +++ b/tests/Controller/HelpControllerTest.php @@ -39,6 +39,14 @@ class HelpControllerTest extends ControllerBaseTest $this->assertContains('Back', $client->getResponse()->getContent()); } + public function testMissingPage() + { + $client = $this->getClientForAuthenticatedUser(); + $this->request($client, '/help/foo'); + $this->assertFalse($client->getResponse()->isSuccessful()); + $this->assertEquals(404, $client->getResponse()->getStatusCode()); + } + public function testValidateRouteDoesNotAllowSpecialChars() { $client = $this->getClientForAuthenticatedUser(); diff --git a/tests/DataFixtures/TimesheetFixtures.php b/tests/DataFixtures/TimesheetFixtures.php index 4247a4d1..c2415f42 100644 --- a/tests/DataFixtures/TimesheetFixtures.php +++ b/tests/DataFixtures/TimesheetFixtures.php @@ -43,9 +43,40 @@ class TimesheetFixtures extends Fixture * @var string */ protected $startDate = '2018-04-01'; + /** + * @var bool + */ + protected $fixedRate = false; + /** + * @var bool + */ + protected $hourlyRate = false; + + /** + * @param bool $fixedRate + * @return TimesheetFixtures + */ + public function setFixedRate(bool $fixedRate) + { + $this->fixedRate = $fixedRate; + + return $this; + } + + /** + * @param bool $hourlyRate + * @return TimesheetFixtures + */ + public function setHourlyRate(bool $hourlyRate) + { + $this->hourlyRate = $hourlyRate; + + return $this; + } /** * @param string|\DateTime $date + * @return TimesheetFixtures */ public function setStartDate($date) { @@ -53,6 +84,8 @@ class TimesheetFixtures extends Fixture $date = $date->format('Y-m-d'); } $this->startDate = $date; + + return $this; } /** @@ -232,6 +265,14 @@ class TimesheetFixtures extends Fixture ->setRate(round(($duration / 3600) * $rate)) ->setBegin($start); + if ($this->fixedRate) { + $entry->setFixedRate(rand(10, 100)); + } + + if ($this->hourlyRate) { + $entry->setHourlyRate($rate); + } + if ($setEndDate) { $entry ->setEnd($end) diff --git a/tests/Entity/TimesheetTest.php b/tests/Entity/TimesheetTest.php index 2093e5ef..ce190d2a 100644 --- a/tests/Entity/TimesheetTest.php +++ b/tests/Entity/TimesheetTest.php @@ -88,8 +88,9 @@ class TimesheetTest extends AbstractEntityTest public function testValidationProjectMismatch() { - $project = (new Project())->setName('foo'); - $project2 = (new Project())->setName('bar'); + $customer = new Customer(); + $project = (new Project())->setName('foo')->setCustomer($customer); + $project2 = (new Project())->setName('bar')->setCustomer($customer); $activity = (new Activity())->setName('hello-world')->setProject($project); $entity = new Timesheet(); @@ -103,6 +104,57 @@ class TimesheetTest extends AbstractEntityTest $this->assertHasViolationForField($entity, 'project'); } + public function testValidationCustomerInvisible() + { + $customer = (new Customer())->setVisible(false); + $project = (new Project())->setName('foo')->setCustomer($customer); + $activity = (new Activity())->setName('hello-world')->setProject($project); + + $entity = new Timesheet(); + $entity + ->setUser(new User()) + ->setActivity($activity) + ->setProject($project) + ->setBegin(new \DateTime()) + ; + + $this->assertHasViolationForField($entity, 'customer'); + } + + public function testValidationProjectInvisible() + { + $customer = new Customer(); + $project = (new Project())->setName('foo')->setCustomer($customer)->setVisible(false); + $activity = (new Activity())->setName('hello-world')->setProject($project); + + $entity = new Timesheet(); + $entity + ->setUser(new User()) + ->setActivity($activity) + ->setProject($project) + ->setBegin(new \DateTime()) + ; + + $this->assertHasViolationForField($entity, 'project'); + } + + public function testValidationActivityInvisible() + { + $customer = new Customer(); + $project = (new Project())->setName('foo')->setCustomer($customer); + $activity = (new Activity())->setName('hello-world')->setProject($project)->setVisible(false); + + $entity = new Timesheet(); + $entity + ->setUser(new User()) + ->setActivity($activity) + ->setProject($project) + ->setBegin(new \DateTime()) + ; + + $this->assertHasViolationForField($entity, 'activity'); + } + public function testValidationEndNotEarlierThanBegin() { $entity = $this->getEntity(); diff --git a/tests/Event/ConfigureAdminMenuEventTest.php b/tests/Event/ConfigureAdminMenuEventTest.php new file mode 100644 index 00000000..d86d7c19 --- /dev/null +++ b/tests/Event/ConfigureAdminMenuEventTest.php @@ -0,0 +1,39 @@ +setLocale('de'); + + $event = new SidebarMenuEvent($request); + $admin = new MenuItemModel('admin', 'foo', 'bar'); + $event->addItem($admin); + $event->addItem(new MenuItemModel('foo', 'foo', 'bar')); + + $sut = new ConfigureAdminMenuEvent($request, $event); + + $this->assertEquals($request, $sut->getRequest()); + $this->assertEquals($event, $sut->getMenu()); + $this->assertEquals($admin, $sut->getAdminMenu()); + } +} diff --git a/tests/Event/DashboardEventTest.php b/tests/Event/DashboardEventTest.php new file mode 100644 index 00000000..5929e0e0 --- /dev/null +++ b/tests/Event/DashboardEventTest.php @@ -0,0 +1,37 @@ +setAlias('foo'); + + $sut = new DashboardEvent($user); + + $this->assertEquals($user, $sut->getUser()); + $this->assertEquals([], $sut->getSections()); + + $section = new DashboardSection('foo'); + $sut->addSection($section); + + $this->assertEquals([$section], $sut->getSections()); + } +} diff --git a/tests/Event/UserPreferenceEventTest.php b/tests/Event/UserPreferenceEventTest.php new file mode 100644 index 00000000..38d3198b --- /dev/null +++ b/tests/Event/UserPreferenceEventTest.php @@ -0,0 +1,57 @@ +setAlias('foo'); + $pref = new UserPreference(); + $pref->setName('foo')->setValue('bar'); + + $sut = new UserPreferenceEvent($user, []); + + $this->assertEquals($user, $sut->getUser()); + $this->assertEquals([], $sut->getPreferences()); + + $sut->addUserPreference($pref); + + $this->assertEquals([$pref], $sut->getPreferences()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testDuplicatePreferenceThrowsException() + { + $user = new User(); + $user->setAlias('foo'); + $pref = new UserPreference(); + $pref->setName('foo')->setValue('bar'); + + $pref2 = new UserPreference(); + $pref2->setName('foo')->setValue('hello'); + + $sut = new UserPreferenceEvent($user, []); + + $sut->addUserPreference($pref); + $sut->addUserPreference($pref2); + } +} diff --git a/tests/Invoice/Renderer/DebugRendererTest.php b/tests/Invoice/Renderer/DebugRendererTest.php index 02d00cb7..9f419961 100644 --- a/tests/Invoice/Renderer/DebugRendererTest.php +++ b/tests/Invoice/Renderer/DebugRendererTest.php @@ -36,7 +36,7 @@ class DebugRendererTest extends AbstractRendererTest $rows = $data['entries']; $this->assertEquals($expectedRows, count($rows)); - foreach($rows as $row) { + foreach ($rows as $row) { $this->assertEntryStructure($row); } @@ -75,7 +75,7 @@ class DebugRendererTest extends AbstractRendererTest 'customer.comment', ]; - foreach($keys as $key) { + foreach ($keys as $key) { $this->assertArrayHasKey($key, $model); } @@ -115,7 +115,7 @@ class DebugRendererTest extends AbstractRendererTest 'entry.customer_id', ]; - foreach($keys as $key) { + foreach ($keys as $key) { $this->assertArrayHasKey($key, $model); }