Release 2.36.0 (#5514)

This commit is contained in:
Kevin Papst
2025-06-02 16:07:48 +02:00
committed by GitHub
parent 594385f741
commit 2f2ebd6293
27 changed files with 466 additions and 257 deletions

383
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -63,24 +63,6 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
} }
public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context) public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context)
{
$errors = [];
/** @var ConstraintViolationInterface $error */
foreach (iterator_to_array($exception->getViolations()) as $error) {
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error);
}
return [
'code' => '400',
'message' => $this->translator->trans($exception->getMessage(), [], 'validators'),
'errors' => [
'children' => $errors
],
];
}
private function getErrorMessage(ConstraintViolationInterface $error): string
{ {
$locale = \Locale::getDefault(); $locale = \Locale::getDefault();
/** @var User $user */ /** @var User $user */
@@ -90,6 +72,24 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
$locale = $user->getLanguage(); $locale = $user->getLanguage();
} }
$errors = [];
/** @var ConstraintViolationInterface $error */
foreach (iterator_to_array($exception->getViolations()) as $error) {
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error, $locale);
}
return [
'code' => '400',
'message' => $this->translator->trans($exception->getMessage(), [], 'validators', $locale),
'errors' => [
'children' => $errors
],
];
}
private function getErrorMessage(ConstraintViolationInterface $error, string $locale): string
{
if (null !== $error->getPlural()) { if (null !== $error->getPlural()) {
return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getPlural()] + $error->getParameters(), 'validators', $locale); return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getPlural()] + $error->getParameters(), 'validators', $locale);
} }

View File

@@ -315,7 +315,7 @@ final class TimesheetController extends BaseApiController
if ($form->isValid()) { if ($form->isValid()) {
try { try {
$this->service->saveNewTimesheet($timesheet); $this->service->saveTimesheet($timesheet);
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
@@ -372,7 +372,7 @@ final class TimesheetController extends BaseApiController
return $this->viewHandler->handle($view); return $this->viewHandler->handle($view);
} }
$this->service->updateTimesheet($timesheet); $this->service->saveTimesheet($timesheet);
$view = new View($timesheet, Response::HTTP_OK); $view = new View($timesheet, Response::HTTP_OK);
$view->getContext()->setGroups(self::GROUPS_ENTITY); $view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -547,7 +547,7 @@ final class TimesheetController extends BaseApiController
$copyTimesheet = clone $timesheet; $copyTimesheet = clone $timesheet;
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet)); $this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
$this->service->saveNewTimesheet($copyTimesheet); $this->service->saveTimesheet($copyTimesheet);
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet)); $this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
$view = new View($copyTimesheet, 200); $view = new View($copyTimesheet, 200);
@@ -571,7 +571,7 @@ final class TimesheetController extends BaseApiController
$timesheet->setExported(!$timesheet->isExported()); $timesheet->setExported(!$timesheet->isExported());
$this->service->updateTimesheet($timesheet); $this->service->saveTimesheet($timesheet);
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY); $view->getContext()->setGroups(self::GROUPS_ENTITY);
@@ -601,7 +601,7 @@ final class TimesheetController extends BaseApiController
$meta->setValue($paramFetcher->get('value')); $meta->setValue($paramFetcher->get('value'));
$this->service->updateTimesheet($timesheet); $this->service->saveTimesheet($timesheet);
$view = new View($timesheet, 200); $view = new View($timesheet, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY); $view->getContext()->setGroups(self::GROUPS_ENTITY);

View File

@@ -97,7 +97,7 @@ class ActivityService
$errors = $this->validator->validate($activity, null, $groups); $errors = $this->validator->validate($activity, null, $groups);
if ($errors->count() > 0) { if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed'); throw new ValidationFailedException($errors);
} }
} }

View File

@@ -17,11 +17,11 @@ final class Constants
/** /**
* The current release version * The current release version
*/ */
public const VERSION = '2.35.1'; public const VERSION = '2.36.0';
/** /**
* The current release: major * 10000 + minor * 100 + patch * The current release: major * 10000 + minor * 100 + patch
*/ */
public const VERSION_ID = 23501; public const VERSION_ID = 23600;
/** /**
* The software name * The software name
*/ */

View File

@@ -152,7 +152,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) { if ($editForm->isSubmitted() && $editForm->isValid()) {
try { try {
$this->service->updateTimesheet($entry); $this->service->saveTimesheet($entry);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
return $this->redirectToRoute($this->getTimesheetRoute()); return $this->redirectToRoute($this->getTimesheetRoute());
@@ -184,7 +184,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($createForm->isSubmitted() && $createForm->isValid()) { if ($createForm->isSubmitted() && $createForm->isValid()) {
try { try {
$this->service->saveNewTimesheet($entry); $this->service->saveTimesheet($entry);
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');
return $this->redirectToRoute($this->getTimesheetRoute()); return $this->redirectToRoute($this->getTimesheetRoute());
@@ -216,7 +216,7 @@ abstract class TimesheetAbstractController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
try { try {
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet)); $this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
$this->service->saveNewTimesheet($copyTimesheet); $this->service->saveTimesheet($copyTimesheet);
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet)); $this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');

View File

@@ -126,7 +126,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
} }
foreach ($newTimesheets as $newTimesheet) { foreach ($newTimesheets as $newTimesheet) {
$this->service->saveNewTimesheet($newTimesheet); $this->service->saveTimesheet($newTimesheet);
} }
$this->flashSuccess('action.update.success'); $this->flashSuccess('action.update.success');

View File

@@ -101,7 +101,7 @@ final class CustomerService
$errors = $this->validator->validate($customer, null, $groups); $errors = $this->validator->validate($customer, null, $groups);
if ($errors->count() > 0) { if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed'); throw new ValidationFailedException($errors);
} }
} }

View File

@@ -59,8 +59,7 @@ class TimesheetEditForm extends AbstractType
$timezone = $options['timezone']; $timezone = $options['timezone'];
$isNew = true; $isNew = true;
if (isset($options['data'])) { if (isset($options['data']) && $options['data'] instanceof Timesheet) {
/** @var Timesheet $entry */
$entry = $options['data']; $entry = $options['data'];
$activity = $entry->getActivity(); $activity = $entry->getActivity();
@@ -338,7 +337,7 @@ class TimesheetEditForm extends AbstractType
function (FormEvent $event) { function (FormEvent $event) {
/** @var Timesheet|null $timesheet */ /** @var Timesheet|null $timesheet */
$timesheet = $event->getData(); $timesheet = $event->getData();
if (null === $timesheet || $timesheet->isRunning()) { if (null === $timesheet || ($timesheet instanceof Timesheet && $timesheet->isRunning())) {
$event->getForm()->get('duration')->setData(null); $event->getForm()->get('duration')->setData(null);
} }
} }

View File

@@ -218,9 +218,7 @@ final class InvoiceModel
} }
/** /**
* Returns the user who is currently creating the invoice. * Returns the user currently creating the invoice.
*
* @return User|null
*/ */
public function getUser(): ?User public function getUser(): ?User
{ {

View File

@@ -105,7 +105,7 @@ final class ProjectService
$errors = $this->validator->validate($project, null, $groups); $errors = $this->validator->validate($project, null, $groups);
if ($errors->count() > 0) { if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed'); throw new ValidationFailedException($errors);
} }
} }

View File

@@ -12,6 +12,7 @@ namespace App\Saml;
use App\Configuration\SamlConfigurationInterface; use App\Configuration\SamlConfigurationInterface;
use App\Saml\Security\SamlAuthenticationFailureHandler; use App\Saml\Security\SamlAuthenticationFailureHandler;
use App\Saml\Security\SamlAuthenticationSuccessHandler; use App\Saml\Security\SamlAuthenticationSuccessHandler;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -40,7 +41,8 @@ class SamlAuthenticator extends AbstractAuthenticator
private readonly SamlAuthenticationFailureHandler $failureHandler, private readonly SamlAuthenticationFailureHandler $failureHandler,
private readonly SamlAuthFactory $samlAuthFactory, private readonly SamlAuthFactory $samlAuthFactory,
private readonly SamlProvider $samlProvider, private readonly SamlProvider $samlProvider,
private readonly SamlConfigurationInterface $configuration private readonly SamlConfigurationInterface $configuration,
private readonly LoggerInterface $logger
) { ) {
} }
@@ -86,6 +88,7 @@ class SamlAuthenticator extends AbstractAuthenticator
// file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML()); // file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML());
if (\count($oneLoginAuth->getErrors()) > 0) { if (\count($oneLoginAuth->getErrors()) > 0) {
$this->logger->critical('SAML login failed: ' . $oneLoginAuth->getLastErrorReason());
throw new AuthenticationException($oneLoginAuth->getLastErrorReason()); throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
} }
@@ -102,7 +105,9 @@ class SamlAuthenticator extends AbstractAuthenticator
if (isset($this->options['username_attribute'])) { if (isset($this->options['username_attribute'])) {
if (!\array_key_exists($this->options['username_attribute'], $attributes)) { if (!\array_key_exists($this->options['username_attribute'], $attributes)) {
throw new \Exception(\sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute'])); $errorMessage = \sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']);
$this->logger->critical($errorMessage);
throw new \Exception($errorMessage);
} }
$username = $attributes[$this->options['username_attribute']][0]; $username = $attributes[$this->options['username_attribute']][0];

View File

@@ -45,12 +45,12 @@ final class TimesheetService
private array $doNotValidateCodes = []; private array $doNotValidateCodes = [];
public function __construct( public function __construct(
private SystemConfiguration $configuration, private readonly SystemConfiguration $configuration,
private TimesheetRepository $repository, private readonly TimesheetRepository $repository,
private TrackingModeService $trackingModeService, private readonly TrackingModeService $trackingModeService,
private EventDispatcherInterface $dispatcher, private readonly EventDispatcherInterface $dispatcher,
private AuthorizationCheckerInterface $auth, private readonly AuthorizationCheckerInterface $auth,
private ValidatorInterface $validator private readonly ValidatorInterface $validator
) { ) {
} }
@@ -101,7 +101,7 @@ final class TimesheetService
public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet
{ {
$this->dispatcher->dispatch(new TimesheetRestartPreEvent($timesheet, $copyFrom)); $this->dispatcher->dispatch(new TimesheetRestartPreEvent($timesheet, $copyFrom));
$this->saveNewTimesheet($timesheet); $this->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
$this->dispatcher->dispatch(new TimesheetRestartPostEvent($timesheet, $copyFrom)); $this->dispatcher->dispatch(new TimesheetRestartPostEvent($timesheet, $copyFrom));
return $timesheet; return $timesheet;
@@ -111,6 +111,7 @@ final class TimesheetService
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped * @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
* @throws InvalidArgumentException for already persisted timesheets * @throws InvalidArgumentException for already persisted timesheets
* @throws AccessDeniedException if user is not allowed to start timesheet * @throws AccessDeniedException if user is not allowed to start timesheet
* @deprecated since 2.36.0 - use saveTimesheet() instead
*/ */
public function saveNewTimesheet(Timesheet $timesheet): Timesheet public function saveNewTimesheet(Timesheet $timesheet): Timesheet
{ {
@@ -150,12 +151,19 @@ final class TimesheetService
return $timesheet; return $timesheet;
} }
public function saveTimesheet(Timesheet $timesheet): Timesheet
{
if ($timesheet->getId() === null) {
return $this->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
} else {
return $this->updateTimesheet($timesheet); // @phpstan-ignore method.deprecated
}
}
/** /**
* Does NOT validate the given timesheet! * Does NOT validate the given timesheet.
* *
* @param Timesheet $timesheet * @deprecated since 2.36.0 - use saveTimesheet() instead
* @return Timesheet
* @throws \Exception
*/ */
public function updateTimesheet(Timesheet $timesheet): Timesheet public function updateTimesheet(Timesheet $timesheet): Timesheet
{ {
@@ -245,7 +253,7 @@ final class TimesheetService
continue; continue;
} }
throw new ValidationFailedException($errors, 'Validation Failed'); throw new ValidationFailedException($errors);
} }
} }
} }

View File

@@ -112,7 +112,7 @@ class UserService
$errors = $this->validator->validate($user, null, $groups); $errors = $this->validator->validate($user, null, $groups);
if ($errors->count() > 0) { if ($errors->count() > 0) {
throw new ValidationFailedException($errors, 'Validation Failed'); throw new ValidationFailedException($errors);
} }
} }

View File

@@ -0,0 +1,26 @@
<?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\Validator\Constraints;
final class TimesheetNegativeDuration extends TimesheetConstraint
{
public const NEGATIVE_DURATION_ERROR = 'kimai-timesheet-negative-duration-01';
protected const ERROR_NAMES = [
self::NEGATIVE_DURATION_ERROR => 'Duration cannot be negative.',
];
public string $message = 'Duration cannot be negative.';
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View File

@@ -0,0 +1,43 @@
<?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\Validator\Constraints;
use App\Entity\Timesheet as TimesheetEntity;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
final class TimesheetNegativeDurationValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!($constraint instanceof TimesheetNegativeDuration)) {
throw new UnexpectedTypeException($constraint, TimesheetNegativeDuration::class);
}
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
throw new UnexpectedTypeException($value, TimesheetEntity::class);
}
if ($value->isRunning()) {
return;
}
$duration = $value->getCalculatedDuration();
if ($duration !== null && $duration < 0) {
$this->context->buildViolation($constraint->message)
->atPath('duration')
->setTranslationDomain('validators')
->setCode(TimesheetNegativeDuration::NEGATIVE_DURATION_ERROR)
->addViolation();
}
}
}

View File

@@ -44,7 +44,7 @@ final class TimesheetZeroDurationValidator extends ConstraintValidator
$duration = $value->getCalculatedDuration(); $duration = $value->getCalculatedDuration();
} }
if ($duration <= 0) { if ($duration === 0) {
$this->context->buildViolation($constraint->message) $this->context->buildViolation($constraint->message)
->atPath('duration') ->atPath('duration')
->setTranslationDomain('validators') ->setTranslationDomain('validators')

View File

@@ -14,7 +14,7 @@ final class ValidationException extends \RuntimeException
public function __construct(string $message = null) public function __construct(string $message = null)
{ {
if ($message === null) { if ($message === null) {
$message = 'Validation failed'; $message = 'Validation Failed';
} }
parent::__construct($message, 400); parent::__construct($message, 400);
} }

View File

@@ -13,10 +13,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface;
final class ValidationFailedException extends \RuntimeException final class ValidationFailedException extends \RuntimeException
{ {
public function __construct(private ConstraintViolationListInterface $violations, ?string $message = null) public function __construct(private readonly ConstraintViolationListInterface $violations, ?string $message = null)
{ {
if ($message === null) { if ($message === null) {
$message = 'Validation failed'; $message = 'Validation Failed';
} }
parent::__construct($message, 400); parent::__construct($message, 400);
} }

View File

@@ -251,7 +251,7 @@ class TimesheetValidationTest extends KernelTestCase
$entity->setBegin($begin); $entity->setBegin($begin);
$entity->setEnd($end); $entity->setEnd($end);
$this->assertHasViolationForField($entity, 'end_date'); $this->assertHasViolationForField($entity, ['end_date', 'duration']);
// allow same begin and end // allow same begin and end
$entity = $this->getEntity(); $entity = $this->getEntity();

View File

@@ -65,6 +65,9 @@ class TimesheetServiceTest extends TestCase
return $service; return $service;
} }
/**
* @group legacy
*/
public function testCannotSavePersistedTimesheetAsNew(): void public function testCannotSavePersistedTimesheetAsNew(): void
{ {
$timesheet = $this->createMock(Timesheet::class); $timesheet = $this->createMock(Timesheet::class);
@@ -75,7 +78,7 @@ class TimesheetServiceTest extends TestCase
$this->expectException(\InvalidArgumentException::class); $this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot create timesheet, already persisted'); $this->expectExceptionMessage('Cannot create timesheet, already persisted');
$sut->saveNewTimesheet($timesheet); $sut->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
} }
public function testCannotStartTimesheet(): void public function testCannotStartTimesheet(): void
@@ -88,7 +91,7 @@ class TimesheetServiceTest extends TestCase
$this->expectException(AccessDeniedHttpException::class); $this->expectException(AccessDeniedHttpException::class);
$this->expectExceptionMessage('You are not allowed to start this timesheet record'); $this->expectExceptionMessage('You are not allowed to start this timesheet record');
$sut->saveNewTimesheet(new Timesheet()); $sut->saveTimesheet(new Timesheet());
} }
public function testSaveNewTimesheetHasValidationError(): void public function testSaveNewTimesheetHasValidationError(): void
@@ -107,7 +110,7 @@ class TimesheetServiceTest extends TestCase
$this->expectException(ValidationFailedException::class); $this->expectException(ValidationFailedException::class);
$this->expectExceptionMessage('Validation Failed'); $this->expectExceptionMessage('Validation Failed');
$sut->saveNewTimesheet(new Timesheet()); $sut->saveTimesheet(new Timesheet());
} }
public function testSaveNewTimesheetStopsActiveRecords(): void public function testSaveNewTimesheetStopsActiveRecords(): void
@@ -134,7 +137,7 @@ class TimesheetServiceTest extends TestCase
$sut = $this->getSut($authorizationChecker, null, null, $repository); $sut = $this->getSut($authorizationChecker, null, null, $repository);
$sut->saveNewTimesheet($newTimesheet); $sut->saveTimesheet($newTimesheet);
} }
public function testSaveNewTimesheetFixesTimezone(): void public function testSaveNewTimesheetFixesTimezone(): void
@@ -155,11 +158,14 @@ class TimesheetServiceTest extends TestCase
$authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true); $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true);
$sut = $this->getSut($authorizationChecker); $sut = $this->getSut($authorizationChecker);
$sut->saveNewTimesheet($timesheet); $sut->saveTimesheet($timesheet);
self::assertEquals('Europe/Paris', $timesheet->getTimezone()); self::assertEquals('Europe/Paris', $timesheet->getTimezone());
} }
/**
* @group legacy
*/
public function testUpdateTimesheetFixesTimezone(): void public function testUpdateTimesheetFixesTimezone(): void
{ {
$user = new User(); $user = new User();
@@ -176,7 +182,7 @@ class TimesheetServiceTest extends TestCase
$sut = $this->getSut(); $sut = $this->getSut();
$sut->updateTimesheet($timesheet); $sut->updateTimesheet($timesheet); // @phpstan-ignore method.deprecated
self::assertEquals('Europe/Paris', $timesheet->getTimezone()); self::assertEquals('Europe/Paris', $timesheet->getTimezone());
} }

View File

@@ -0,0 +1,25 @@
<?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\Tests\Validator\Constraints;
use App\Validator\Constraints\TimesheetConstraint;
use App\Validator\Constraints\TimesheetNegativeDuration;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Validator\Constraints\TimesheetNegativeDuration
*/
class TimesheetNegativeDurationTest extends TestCase
{
public function testIsTimesheetConstraint(): void
{
self::assertInstanceOf(TimesheetConstraint::class, new TimesheetNegativeDuration());
}
}

View File

@@ -0,0 +1,90 @@
<?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\Tests\Validator\Constraints;
use App\Entity\Timesheet;
use App\Validator\Constraints\TimesheetNegativeDuration;
use App\Validator\Constraints\TimesheetNegativeDurationValidator;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @covers \App\Validator\Constraints\TimesheetNegativeDuration
* @covers \App\Validator\Constraints\TimesheetNegativeDurationValidator
* @extends ConstraintValidatorTestCase<TimesheetNegativeDurationValidator>
*/
class TimesheetNegativeDurationValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator(): TimesheetNegativeDurationValidator
{
return new TimesheetNegativeDurationValidator();
}
public function testConstraintIsInvalid(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new Timesheet(), new NotBlank());
}
public function testInvalidValueThrowsException(): void
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
}
public function testNegativeDurationIsNotAllowed(): void
{
$begin = new \DateTime();
$timesheet = new Timesheet();
$timesheet->setBegin(clone $begin);
$timesheet->setEnd(clone $begin);
$timesheet->setBreak(3600);
$this->validator->validate($timesheet, new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
$this->buildViolation('Duration cannot be negative.')
->atPath('property.path.duration')
->setCode(TimesheetNegativeDuration::NEGATIVE_DURATION_ERROR)
->assertRaised();
}
public function testZeroDurationIsAllowed(): void
{
$begin = new \DateTime();
$timesheet = new Timesheet();
$timesheet->setBegin(clone $begin);
$timesheet->setEnd(clone $begin);
$this->validator = $this->createValidator();
$this->validator->initialize($this->context);
$this->validator->validate($timesheet, new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
$this->assertNoViolation();
}
public function testDoesNotTriggerOnRunningTimesheet(): void
{
$begin = new \DateTime();
$timesheet = new Timesheet();
$timesheet->setBegin(clone $begin);
$timesheet->setBreak(3600);
$this->validator = $this->createValidator();
$this->validator->initialize($this->context);
$this->validator->validate($timesheet, new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
$this->assertNoViolation();
}
}

View File

@@ -21,7 +21,7 @@ class ValidationExceptionTest extends TestCase
{ {
$sut = new ValidationException(); $sut = new ValidationException();
self::assertEquals(400, $sut->getCode()); self::assertEquals(400, $sut->getCode());
self::assertEquals('Validation failed', $sut->getMessage()); self::assertEquals('Validation Failed', $sut->getMessage());
} }
public function testConstruct(): void public function testConstruct(): void

View File

@@ -23,7 +23,7 @@ class ValidationFailedExceptionTest extends TestCase
$list = new ConstraintViolationList(); $list = new ConstraintViolationList();
$sut = new ValidationFailedException($list); $sut = new ValidationFailedException($list);
self::assertEquals(400, $sut->getCode()); self::assertEquals(400, $sut->getCode());
self::assertEquals('Validation failed', $sut->getMessage()); self::assertEquals('Validation Failed', $sut->getMessage());
self::assertSame($list, $sut->getViolations()); self::assertSame($list, $sut->getViolations());
} }

View File

@@ -162,6 +162,10 @@
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source> <source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
<target>Ausgewählter Zeitraum kann nicht gesperrt werden: unbestätigte Abwesenheitsanträge stehen an.</target> <target>Ausgewählter Zeitraum kann nicht gesperrt werden: unbestätigte Abwesenheitsanträge stehen an.</target>
</trans-unit> </trans-unit>
<trans-unit id="KVyAzBQ" resname="Duration cannot be negative.">
<source>Duration cannot be negative.</source>
<target>Die Dauer kann nicht negativ sein.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@@ -162,6 +162,10 @@
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source> <source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
<target>Selected period cannot be locked: unconfirmed absence requests are pending.</target> <target>Selected period cannot be locked: unconfirmed absence requests are pending.</target>
</trans-unit> </trans-unit>
<trans-unit id="KVyAzBQ" resname="Duration cannot be negative.">
<source>Duration cannot be negative.</source>
<target>Duration cannot be negative.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>