added setting to limit the maximum length of a timesheet record (#2612)

This commit is contained in:
Kevin Papst
2021-06-12 01:05:33 +02:00
committed by GitHub
parent 53e01177d9
commit 7460647d58
19 changed files with 366 additions and 28 deletions

View File

@@ -217,7 +217,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
/**
* @param Response $response
* @param string[] $failedFields
* @param array<int, string>|array<string, mixed> $failedFields
* @param bool $extraFields test for the error "This form should not contain extra fields"
*/
protected function assertApiCallValidationError(Response $response, array $failedFields, bool $extraFields = false)
@@ -235,9 +235,21 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
self::assertArrayHasKey('children', $result['errors']);
$data = $result['errors']['children'];
foreach ($failedFields as $fieldName) {
self::assertArrayHasKey($fieldName, $data, sprintf('Could not find validation error for field: %s', $fieldName));
foreach ($failedFields as $key => $value) {
$messages = [];
$fieldName = $value;
if (\is_string($key)) {
$fieldName = $key;
$messages = $value;
if (!\is_array($messages)) {
$messages = [$value];
}
}
self::assertArrayHasKey($fieldName, $data, sprintf('Could not find validation error for field "%s" in list: %s', $fieldName, implode(', ', $failedFields)));
self::assertArrayHasKey('errors', $data[$fieldName], sprintf('Field %s has no validation problem', $fieldName));
foreach ($messages as $i => $message) {
self::assertEquals($message, $data[$fieldName]['errors'][$i]);
}
}
$foundErrors = [];

View File

@@ -389,7 +389,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$data = [
'activity' => 1,
'project' => 1,
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'begin' => ($dateTime->createDateTime('-8 hours'))->format('Y-m-d H:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo',
'fixedRate' => 2016,
@@ -402,7 +402,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
}
@@ -413,7 +413,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$data = [
'activity' => 1,
'project' => 1,
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'begin' => ($dateTime->createDateTime('-8 hours'))->format('Y-m-d H:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo',
'fixedRate' => 2016,
@@ -426,7 +426,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntityFull', $result);
$this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
}
@@ -443,7 +443,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
'activity' => 1,
'project' => 1,
'user' => $user->getId(),
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'begin' => ($dateTime->createDateTime('- 8 hours'))->format('Y-m-d H:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo',
'fixedRate' => 2016,
@@ -744,7 +744,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->importFixtureForUser(User::ROLE_USER);
$start = new \DateTime('-10 days');
$start = new \DateTime('-8 hours');
$fixture = new TimesheetFixtures();
$fixture
@@ -752,7 +752,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
->setHourlyRate(true)
->setAmount(0)
->setUser($this->getUserByRole(User::ROLE_USER))
->setStartDate($start)
->setFixedStartDate($start)
->setAmountRunning(1)
;
$timesheets = $this->importFixture($fixture);
@@ -772,6 +772,30 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
}
public function testStopActionTriggersValidationOnLongRunning()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->setSystemConfiguration('timesheet.rules.long_running_duration', 750);
$this->importFixtureForUser(User::ROLE_USER);
$start = new \DateTime('-13 hours');
$fixture = new TimesheetFixtures();
$fixture
->setFixedRate(true)
->setHourlyRate(true)
->setAmount(0)
->setUser($this->getUserByRole(User::ROLE_USER))
->setFixedStartDate($start)
->setAmountRunning(1)
;
$timesheets = $this->importFixture($fixture);
$id = $timesheets[0]->getId();
$this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH');
$this->assertApiCallValidationError($client->getResponse(), ['duration' => 'Maximum 12:30 hours allowed.']);
}
public function testStopActionFailsOnStoppedEntry()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
@@ -995,7 +1019,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$data = [
'activity' => 1,
'project' => 1,
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
'begin' => ($dateTime->createDateTime('- 8 hours'))->format('Y-m-d H:m:0'),
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
'description' => 'foo',
'fixedRate' => 2016,
@@ -1008,7 +1032,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
$this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH');
@@ -1018,7 +1042,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
$this->assertIsArray($result);
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
$this->assertNotEmpty($result['id']);
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $result['rate']);
}

View File

@@ -304,10 +304,10 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertEquals(50, $timesheet->getRate());
$expected = new \DateTime('2018-08-02T20:00:00');
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getBegin()->format(\DateTimeInterface::ATOM));
$expected = new \DateTime('2018-08-02T20:30:00');
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getEnd()->format(\DateTimeInterface::ATOM));
}
public function testCreateActionWithFromAndToValuesTwice()
@@ -338,10 +338,10 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertEquals(50, $timesheet->getRate());
$expected = new \DateTime('2018-08-02T20:00:00');
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getBegin()->format(\DateTimeInterface::ATOM));
$expected = new \DateTime('2018-08-02T20:30:00');
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getEnd()->format(\DateTimeInterface::ATOM));
// create a second entry that is overlapping
$this->request($client, '/timesheet/create?from=2018-08-02T20%3A02%3A00&to=2018-08-02T20%3A20%3A00');
@@ -489,10 +489,10 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertEquals(800, $timesheet->getRate());
$expected = new \DateTime('2018-08-02T10:00:00');
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getBegin()->format(\DateTimeInterface::ATOM));
$expected = new \DateTime('2018-08-02T18:00:00');
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getEnd()->format(\DateTimeInterface::ATOM));
$this->assertEquals(['one', 'two', 'three'], $timesheet->getTagsAsArray());
}
@@ -502,9 +502,9 @@ class TimesheetControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser();
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setAmount(1);
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
$fixture->setStartDate('2017-05-01');
$fixture->setFixedStartDate(new \DateTime('-2 hours'));
$timesheets = $this->importFixture($fixture);
$id = $timesheets[0]->getId();
@@ -527,6 +527,10 @@ class TimesheetControllerTest extends ControllerBaseTest
]
]);
if (!$client->getResponse()->isRedirect()) {
dd($response->getStatusCode(), $response->getContent());
}
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
@@ -645,7 +649,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$fixture->setCallback(function (Timesheet $timesheet) {
$timesheet->setDescription('Testing is fun!');
$end = clone $timesheet->getBegin();
$end->modify('+ 16 hours');
$end->modify('+ 8 hours');
$timesheet->setEnd($end);
$timesheet->setFixedRate(2016);
$timesheet->setHourlyRate(127);
@@ -675,7 +679,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertEquals(2016, $timesheet->getRate());
$this->assertEquals(127, $timesheet->getHourlyRate());
$this->assertEquals(2016, $timesheet->getFixedRate());
$this->assertTrue($timesheet->getDuration() == 57600 || $timesheet->getDuration() == 57660); // 1 minute rounding might be applied
$this->assertTrue($timesheet->getDuration() == 28800 || $timesheet->getDuration() == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $timesheet->getRate());
}
}

View File

@@ -265,13 +265,12 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$em = $this->getEntityManager();
$user = $this->getUserByRole(User::ROLE_USER);
$teamlead = $this->getUserByRole(User::ROLE_TEAMLEAD);
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($user);
$fixture->setStartDate('2017-05-01');
$fixture->setFixedStartDate(new \DateTime('-2 hours'));
$timesheets = $this->importFixture($fixture);
$id = $timesheets[0]->getId();
@@ -420,7 +419,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$fixture->setCallback(function (Timesheet $timesheet) {
$timesheet->setDescription('Testing is fun!');
$end = clone $timesheet->getBegin();
$end->modify('+ 16 hours');
$end->modify('+ 8 hours');
$timesheet->setEnd($end);
$timesheet->setFixedRate(2016);
$timesheet->setHourlyRate(127);
@@ -450,7 +449,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
$this->assertEquals(2016, $timesheet->getRate());
$this->assertEquals(127, $timesheet->getHourlyRate());
$this->assertEquals(2016, $timesheet->getFixedRate());
$this->assertTrue($timesheet->getDuration() == 57600 || $timesheet->getDuration() == 57660); // 1 minute rounding might be applied
$this->assertTrue($timesheet->getDuration() == 28800 || $timesheet->getDuration() == 28860); // 1 minute rounding might be applied
$this->assertEquals(2016, $timesheet->getRate());
}
}

View File

@@ -48,6 +48,10 @@ final class TimesheetFixtures implements TestFixture
* @var \DateTime
*/
private $startDate;
/**
* @var \DateTime
*/
private $fixedStartDate;
/**
* @var bool
*/
@@ -129,6 +133,13 @@ final class TimesheetFixtures implements TestFixture
return $this;
}
public function setFixedStartDate(\DateTime $date): TimesheetFixtures
{
$this->fixedStartDate = $date;
return $this;
}
public function setAmountRunning(int $amount): TimesheetFixtures
{
$this->running = $amount;
@@ -312,6 +323,10 @@ final class TimesheetFixtures implements TestFixture
private function getDateTime(int $i): \DateTime
{
if ($this->fixedStartDate !== null) {
return $this->fixedStartDate;
}
if ($this->startDate === null) {
$this->startDate = new \DateTime('2018-04-01');
}

View File

@@ -204,6 +204,8 @@ class AppExtensionTest extends TestCase
'lockdown_grace_period' => null,
'allow_overbooking_budget' => true,
'lockdown_period_timezone' => null,
'break_warning_duration' => 0,
'long_running_duration' => 0,
],
'default_begin' => 'now',
'duration_increment' => null,

View File

@@ -289,6 +289,8 @@ class ConfigurationTest extends TestCase
'lockdown_grace_period' => null,
'allow_overbooking_budget' => true,
'lockdown_period_timezone' => null,
'break_warning_duration' => 0,
'long_running_duration' => 0,
],
'duration_increment' => null,
'time_increment' => null,

View File

@@ -0,0 +1,117 @@
<?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\Configuration\ConfigLoaderInterface;
use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet;
use App\Validator\Constraints\TimesheetLongRunning;
use App\Validator\Constraints\TimesheetLongRunningValidator;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @covers \App\Validator\Constraints\TimesheetLongRunningValidator
*/
class TimesheetLongRunningValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator()
{
return $this->createMyValidator(120);
}
protected function createMyValidator(int $minutes)
{
$loader = $this->createMock(ConfigLoaderInterface::class);
$config = new SystemConfiguration($loader, [
'timesheet' => [
'rules' => [
'long_running_duration' => $minutes,
],
],
]);
return new TimesheetLongRunningValidator($config);
}
public function testConstraintIsInvalid()
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new Timesheet(), new NotBlank());
}
public function testInvalidValueThrowsException()
{
$this->expectException(UnexpectedTypeException::class);
$this->validator->validate(new NotBlank(), new TimesheetLongRunning(['message' => 'myMessage']));
}
public function testLongRunningTriggers()
{
$begin = new \DateTime();
$end = new \DateTime('+10 hour');
$timesheet = new Timesheet();
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
$this->validator->validate($timesheet, new TimesheetLongRunning());
$this->buildViolation('Maximum duration of {{ value }} hours exceeded.')
->atPath('property.path.duration')
->setParameter('{{ value }}', '02:00')
->setCode(TimesheetLongRunning::LONG_RUNNING)
->assertRaised();
}
public function testLongRunningNotTriggersIfConfiguredToZero()
{
$this->validator = $this->createMyValidator(0);
$this->validator->initialize($this->context);
$begin = new \DateTime();
$end = new \DateTime('+10 hour');
$timesheet = new Timesheet();
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
$this->validator->validate($timesheet, new TimesheetLongRunning());
$this->assertNoViolation();
}
public function testLongRunningNotTriggersIfDurationIsLowerThan()
{
$this->validator = $this->createMyValidator(121);
$this->validator->initialize($this->context);
$begin = new \DateTime();
$end = new \DateTime('+2 hour');
$timesheet = new Timesheet();
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
$this->validator->validate($timesheet, new TimesheetLongRunning());
$this->assertNoViolation();
}
public function testNotTriggersOnRunningRecord()
{
$begin = new \DateTime('-10 hour');
$timesheet = new Timesheet();
$timesheet->setBegin($begin);
$this->validator->validate($timesheet, new TimesheetLongRunning());
$this->assertNoViolation();
}
}