convert timesheets to UTC with support for user timezone (#372)

This commit is contained in:
Kevin Papst
2019-02-08 18:24:33 +01:00
committed by GitHub
parent 8dcc18dde3
commit d00596d297
58 changed files with 1637 additions and 207 deletions

View File

@@ -0,0 +1,97 @@
<?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\Command;
use App\Command\BashResult;
use App\Command\RunCodeSnifferCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
/**
* @coversDefaultClass \App\Command\RunCodeSnifferCommand
* @group integration
*/
class RunCodeSnifferCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
/**
* @var TestBashExecutor
*/
protected $executor;
/**
* @var string
*/
protected $directory;
protected function setUp()
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->directory = realpath(__DIR__ . '/../../');
$this->executor = new TestBashExecutor($this->directory);
$this->application->add(new RunCodeSnifferCommand($this->executor, $this->directory));
}
public function testSuccessCommandNoOptions()
{
$command = $this->assertSuccessCommand([]);
$this->assertStringStartsWith('/vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none --format=txt', $command);
}
public function testSuccessCommandFix()
{
$command = $this->assertSuccessCommand(['--fix' => true]);
$this->assertStringStartsWith('/vendor/bin/php-cs-fixer fix', $command);
}
public function testSuccessCommandCheckstyle()
{
$command = $this->assertSuccessCommand(['--checkstyle' => 'phpcs.txt']);
$this->assertStringStartsWith('/vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none > ', $command);
$this->assertStringEndsWith('phpcs.txt', $command);
}
protected function assertSuccessCommand(array $options)
{
$result = new BashResult(0, 'FooBar');
$this->executor->setResult($result);
$command = $this->application->find('kimai:phpcs');
$commandTester = new CommandTester($command);
$inputs = array_merge(['command' => $command->getName()], $options);
$commandTester->execute($inputs);
$output = $commandTester->getDisplay();
$this->assertContains('FooBar', $output);
$this->assertContains('[OK] All source files have proper code styles', $output);
return $this->executor->getCommand();
}
public function testFailureCommand()
{
$result = new BashResult(1, 'BarFoo');
$this->executor->setResult($result);
$command = $this->application->find('kimai:phpcs');
$commandTester = new CommandTester($command);
$inputs = array_merge(['command' => $command->getName()], ['--fix' => true]);
$commandTester->execute($inputs);
$output = $commandTester->getDisplay();
$this->assertContains('BarFoo', $output);
$this->assertContains('[ERROR] Found problems while checking your code styles', $output);
}
}

View File

@@ -0,0 +1,81 @@
<?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\Command;
use App\Command\BashResult;
use App\Command\RunIntegrationTestsCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
/**
* @coversDefaultClass \App\Command\RunIntegrationTestsCommand
* @group integration
*/
class RunIntegrationTestsCommandTest extends RunUnitTestsCommandTest
{
/**
* @var Application
*/
protected $application;
/**
* @var TestBashExecutor
*/
protected $executor;
/**
* @var string
*/
protected $directory;
protected function setUp()
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->directory = realpath(__DIR__ . '/../../');
$this->executor = new TestBashExecutor($this->directory);
$this->application->add(new RunIntegrationTestsCommand($this->executor, $this->directory));
}
public function testSuccessCommand()
{
$result = new BashResult(0, 'FooBar');
$this->executor->setResult($result);
$command = $this->application->find('kimai:test-integration');
$commandTester = new CommandTester($command);
$inputs = array_merge(['command' => $command->getName()], []);
$commandTester->execute($inputs);
$output = $commandTester->getDisplay();
$this->assertContains('FooBar', $output);
$this->assertContains('[OK] All tests were successful', $output);
$this->assertStringStartsWith('/bin/phpunit --group integration', $this->executor->getCommand());
$this->assertContains($this->directory, $this->executor->getCommand());
}
public function testFailureCommand()
{
$result = new BashResult(1, 'BarFoo');
$this->executor->setResult($result);
$command = $this->application->find('kimai:test-integration');
$commandTester = new CommandTester($command);
$inputs = array_merge(['command' => $command->getName()], []);
$commandTester->execute($inputs);
$output = $commandTester->getDisplay();
$this->assertContains('BarFoo', $output);
$this->assertContains('[ERROR] Found problems while running tests', $output);
$this->assertStringStartsWith('/bin/phpunit --group integration', $this->executor->getCommand());
$this->assertContains($this->directory, $this->executor->getCommand());
}
}

View File

@@ -0,0 +1,82 @@
<?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\Command;
use App\Command\BashResult;
use App\Command\RunUnitTestsCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
/**
* @coversDefaultClass \App\Command\RunUnitTestsCommand
* @group integration
*/
class RunUnitTestsCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
/**
* @var TestBashExecutor
*/
protected $executor;
/**
* @var string
*/
protected $directory;
protected function setUp()
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);
$this->directory = realpath(__DIR__ . '/../../');
$this->executor = new TestBashExecutor($this->directory);
$this->application->add(new RunUnitTestsCommand($this->executor, $this->directory));
}
public function testSuccessCommand()
{
$result = new BashResult(0, 'FooBar');
$this->executor->setResult($result);
$command = $this->application->find('kimai:test-unit');
$commandTester = new CommandTester($command);
$inputs = array_merge(['command' => $command->getName()], []);
$commandTester->execute($inputs);
$output = $commandTester->getDisplay();
$this->assertContains('FooBar', $output);
$this->assertContains('[OK] All tests were successful', $output);
$this->assertStringStartsWith('/bin/phpunit --exclude-group integration', $this->executor->getCommand());
$this->assertContains($this->directory, $this->executor->getCommand());
}
public function testFailureCommand()
{
$result = new BashResult(1, 'BarFoo');
$this->executor->setResult($result);
$command = $this->application->find('kimai:test-unit');
$commandTester = new CommandTester($command);
$inputs = array_merge(['command' => $command->getName()], []);
$commandTester->execute($inputs);
$output = $commandTester->getDisplay();
$this->assertContains('BarFoo', $output);
$this->assertContains('[ERROR] Found problems while running tests', $output);
$this->assertStringStartsWith('/bin/phpunit --exclude-group integration', $this->executor->getCommand());
$this->assertContains($this->directory, $this->executor->getCommand());
}
}

View File

@@ -0,0 +1,55 @@
<?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\Command;
use App\Command\BashExecutor;
use App\Command\BashResult;
class TestBashExecutor extends BashExecutor
{
/**
* @var BashResult
*/
protected $result;
/**
* @var string
*/
protected $command;
/**
* @param BashResult $result
* @return $this
*/
public function setResult(BashResult $result)
{
$this->result = $result;
return $this;
}
/**
* @return string
*/
public function getCommand(): string
{
return $this->command;
}
/**
* @param string $command
* @return BashResult
*/
public function execute(string $command)
{
$this->command = $command;
return $this->result;
}
}

View File

@@ -256,7 +256,7 @@ class ProfileControllerTest extends ControllerBaseTest
'user_preferences_form' => [
'preferences' => [
['name' => UserPreference::HOURLY_RATE, 'value' => 37.5],
// ['name' => 'timezone', 'value' => 'America/Creston'],
['name' => 'timezone', 'value' => 'America/Creston'],
['name' => 'language', 'value' => 'ar'],
['name' => UserPreference::SKIN, 'value' => 'blue'],
['name' => 'theme.fixed_layout', 'value' => false],
@@ -280,7 +280,7 @@ class ProfileControllerTest extends ControllerBaseTest
$user = $this->getUserByName($em, $username);
$this->assertEquals($hourlyRate, $user->getPreferenceValue(UserPreference::HOURLY_RATE));
//$this->assertEquals('', $user->getPreferenceValue('America/Creston'));
$this->assertEquals('', $user->getPreferenceValue('America/Creston'));
$this->assertEquals('ar', $user->getPreferenceValue('language'));
$this->assertEquals('blue', $user->getPreferenceValue(UserPreference::SKIN));
$this->assertEquals(false, $user->getPreferenceValue('theme.fixed_layout'));

View File

@@ -116,6 +116,11 @@ class TimesheetControllerTest extends ControllerBaseTest
$client->submit($form, [
'timesheet_edit_form' => [
'description' => 'Testing is fun!',
// begin is always pre-filled with the current datetime
// 'begin' => null,
// end must be allowed to be null, to start a record
// there was a bug with end begin required, so we manually set this field to be empty
'end' => null,
'project' => 1,
'activity' => 1,
]

View File

@@ -0,0 +1,105 @@
<?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\Doctrine;
use App\Doctrine\UTCDateTimeType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @covers \App\Doctrine\UTCDateTimeType
*/
class UTCDateTimeTypeTest extends KernelTestCase
{
/**
* @var AbstractPlatform
*/
private $platform;
/**
* {@inheritdoc}
*/
protected function setUp()
{
$kernel = self::bootKernel();
$registry = $kernel->getContainer()->get('doctrine');
/** @var \Doctrine\DBAL\Connection $connection */
$connection = $registry->getConnection();
$this->platform = $connection->getDatabasePlatform();
}
public function testGetUtc()
{
Type::overrideType(Type::DATETIME, UTCDateTimeType::class);
/** @var UTCDateTimeType $type */
$type = Type::getType(Type::DATETIME);
$this->assertInstanceOf(UTCDateTimeType::class, $type);
$utc = $type::getUtc();
$this->assertSame($utc, $type::getUtc());
$this->assertEquals('UTC', $type::getUtc()->getName());
}
public function testConvertToDatabaseValue()
{
Type::overrideType(Type::DATETIME, UTCDateTimeType::class);
/** @var UTCDateTimeType $type */
$type = Type::getType(Type::DATETIME);
$result = $type->convertToDatabaseValue(null, $this->platform);
$this->assertNull($result);
$berlinTz = new \DateTimeZone('Europe/Berlin');
$date = new \DateTime('2019-01-17 13:30:00');
$date->setTimezone($berlinTz);
$this->assertEquals('Europe/Berlin', $date->getTimezone()->getName());
$expected = clone $date;
$expected->setTimezone($type::getUtc());
$bla = $expected->format($this->platform->getDateTimeFormatString());
/** @var \DateTime $result */
$result = $type->convertToDatabaseValue($date, $this->platform);
$this->assertEquals($bla, $result);
}
public function testConvertToPHPValue()
{
Type::overrideType(Type::DATETIME, UTCDateTimeType::class);
/** @var UTCDateTimeType $type */
$type = Type::getType(Type::DATETIME);
$result = $type->convertToPHPValue(null, $this->platform);
$this->assertNull($result);
$result = $type->convertToPHPValue('2019-01-17 13:30:00', $this->platform);
$this->assertInstanceOf(\DateTime::class, $result);
$this->assertEquals('UTC', $result->getTimezone()->getName());
$result = $result->format($this->platform->getDateTimeFormatString());
$this->assertEquals('2019-01-17 13:30:00', $result);
}
/**
* @expectedException \Doctrine\DBAL\Types\ConversionException
*/
public function testConvertToPHPValueWithInvalidValue()
{
Type::overrideType(Type::DATETIME, UTCDateTimeType::class);
/** @var UTCDateTimeType $type */
$type = Type::getType(Type::DATETIME);
$type->convertToPHPValue('201xx01-17 13:30:00', $this->platform);
}
}

View File

@@ -68,6 +68,7 @@ abstract class AbstractCalculatorTest extends TestCase
$timesheet = new Timesheet();
$timesheet
->setDescription('timesheet description')
->setBegin(new \DateTime())
->setDuration(3600)
->setRate(293.27)
->setUser(new User())

View File

@@ -44,6 +44,15 @@ class DebugRenderer implements RendererInterface
return $date->format('d.m.Y');
}
/**
* @param \DateTime $date
* @return mixed
*/
protected function getFormattedTime(\DateTime $date)
{
return $date->format('H:i');
}
/**
* @param $amount
* @return mixed

View File

@@ -9,6 +9,8 @@
namespace App\Tests\Repository;
use App\Entity\Activity;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Repository\Query\BaseQuery;
@@ -93,4 +95,28 @@ class TimesheetRepositoryTest extends AbstractRepositoryTest
$this->assertTrue($result);
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
}
public function testSave()
{
$em = $this->getEntityManager();
$activityRepository = $em->getRepository(Activity::class);
$activity = $activityRepository->find(1);
$projectRepository = $em->getRepository(Project::class);
$project = $projectRepository->find(1);
$user = $this->getUserByRole($em, User::ROLE_USER);
$repository = $em->getRepository(Timesheet::class);
$timesheet = new Timesheet();
$timesheet
->setBegin(new \DateTime())
->setEnd(new \DateTime())
->setDescription('foo')
->setUser($user)
->setActivity($activity)
->setProject($project);
$this->assertNull($timesheet->getId());
$repository->save($timesheet);
$this->assertEquals(1, $timesheet->getId());
}
}

View File

@@ -0,0 +1,57 @@
<?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\Model;
use App\Entity\User;
use App\Security\UserChecker;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @covers \App\Security\UserChecker
*/
class UserCheckerTest extends TestCase
{
public function testCheckPreAuth()
{
$sut = new UserChecker();
$user = new User();
try {
$sut->checkPreAuth($user);
} catch (\Exception $ex) {
$this->fail('UserChecker should not throw exception in checkPreAuth()');
}
$this->assertTrue(true);
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\LockedException
*/
public function testDisabledCannotLogin()
{
$sut = new UserChecker();
$user = new User();
$user->setEnabled(false);
$sut->checkPostAuth($user);
}
public function testCheckPostAuth()
{
$sut = new UserChecker();
$mock = $this->getMockBuilder(UserInterface::class)->setMethods(['isEnabled'])->getMockForAbstractClass();
$mock->expects($this->never())->method('isEnabled')->willReturn(false);
$sut->checkPostAuth($mock);
$this->assertTrue(true);
}
}

View File

@@ -163,10 +163,13 @@ class RateCalculatorTest extends TestCase
*/
public function testCalculateWithRules($rules, $expectedFactor)
{
$seconds = 41837;
$seconds = 31837;
$end = new \DateTime();
$end->setTimezone(new \DateTimeZone('UTC'));
$end->setTime(12, 0, 0);
$start = clone $end;
$start->setTimezone(new \DateTimeZone('UTC'));
$start->setTimestamp($end->getTimestamp() - $seconds);
$record = new Timesheet();
@@ -191,6 +194,7 @@ class RateCalculatorTest extends TestCase
public function getRuleDefinitions()
{
$start = new \DateTime();
$start->setTimezone(new \DateTimeZone('UTC'));
$start->setTime(12, 0, 0);
$day = $start->format('l');
@@ -219,7 +223,7 @@ class RateCalculatorTest extends TestCase
'factor' => 2.0
],
'weekdays' => [
'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'days' => ['MonDay', 'tUEsdAy', 'WEdnesday', 'THursday', 'friDay', 'SATURday', 'sunDAY'],
'factor' => 1.5
],
],

View File

@@ -40,7 +40,7 @@ class DateExtensionsTest extends TestCase
public function testGetFilters()
{
$filters = ['month_name', 'date_short', 'date_time'];
$filters = ['month_name', 'date_short', 'date_time', 'time'];
$sut = $this->getSut('de', []);
$twigFilters = $sut->getFilters();
$this->assertCount(count($filters), $twigFilters);
@@ -119,4 +119,13 @@ class DateExtensionsTest extends TestCase
[new \DateTime('2016-12-23'), 'month.12'],
];
}
public function testTime()
{
$time = new \DateTime('2016-06-23');
$time->setTime(17, 53, 23);
$sut = $this->getSut('en', []);
$this->assertEquals('17:53', $sut->time($time));
}
}

View File

@@ -22,5 +22,36 @@ class MarkdownTest extends TestCase
$sut = new Markdown();
$this->assertEquals('<p><em>test</em></p>', $sut->toHtml('*test*'));
$this->assertEquals('<h1 id="foobar">foobar</h1>', $sut->toHtml('# foobar'));
$html = <<<'EOT'
<p>foo bar</p>
<ul>
<li>sdfasdfasdf</li>
<li>asdfasdfasdf</li>
</ul>
<h1 id="test">test</h1>
<p>asdfasdfa</p>
<pre><code>ssdfsdf</code></pre>
<p>sdfsdf <a href="#test-1">asdfasdf</a> asdfasdf</p>
<h1 id="test-1">test</h1>
<p>aasdfasdf</p>
EOT;
$markdown = <<<EOT
foo bar
- sdfasdfasdf
- asdfasdfasdf
# test
asdfasdfa
ssdfsdf
sdfsdf [asdfasdf](#test-1) asdfasdf
# test
aasdfasdf
EOT;
$this->assertEquals($html, $sut->toHtml($markdown));
}
}

View File

@@ -0,0 +1,159 @@
<?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\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
use App\Validator\Constraints\TimesheetValidator;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @covers \App\Validator\Constraints\TimesheetValidator
*/
class TimesheetValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator()
{
$options = [
'allow_future_times' => false
];
return new TimesheetValidator($options);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
*/
public function testConstraintIsInvalid()
{
$this->validator->validate('foo', new NotBlank());
}
public function testEmptyTimesheet()
{
$timesheet = new Timesheet();
$this->validator->validate($timesheet, new TimesheetConstraint(['message' => 'myMessage']));
$this->buildViolation('You must submit a begin date.')
->atPath('property.path.begin')
->setCode(TimesheetConstraint::MISSING_BEGIN_ERROR)
->buildNextViolation('A timesheet must have an activity.')
->atPath('property.path.activity')
->setCode(TimesheetConstraint::MISSING_ACTIVITY_ERROR)
->buildNextViolation('A timesheet must have a project.')
->atPath('property.path.project')
->setCode(TimesheetConstraint::MISSING_PROJECT_ERROR)
->assertRaised();
}
public function testFutureBegin()
{
$begin = new \DateTime('+10 hour');
$timesheet = new Timesheet();
$timesheet->setBegin($begin);
$this->validator->validate($timesheet, new TimesheetConstraint(['message' => 'myMessage']));
$this->buildViolation('The begin date cannot be in the future.')
->atPath('property.path.begin')
->setCode(TimesheetConstraint::BEGIN_IN_FUTURE_ERROR)
->buildNextViolation('A timesheet must have an activity.')
->atPath('property.path.activity')
->setCode(TimesheetConstraint::MISSING_ACTIVITY_ERROR)
->buildNextViolation('A timesheet must have a project.')
->atPath('property.path.project')
->setCode(TimesheetConstraint::MISSING_PROJECT_ERROR)
->assertRaised();
}
public function testEndBeforeBegin()
{
$end = new \DateTime('-10 hour');
$begin = new \DateTime('-1 hour');
$timesheet = new Timesheet();
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
$this->validator->validate($timesheet, new TimesheetConstraint(['message' => 'myMessage']));
$this->buildViolation('End date must not be earlier then start date.')
->atPath('property.path.end')
->setCode(TimesheetConstraint::END_BEFORE_BEGIN_ERROR)
->buildNextViolation('A timesheet must have an activity.')
->atPath('property.path.activity')
->setCode(TimesheetConstraint::MISSING_ACTIVITY_ERROR)
->buildNextViolation('A timesheet must have a project.')
->atPath('property.path.project')
->setCode(TimesheetConstraint::MISSING_PROJECT_ERROR)
->assertRaised();
}
public function testProjectMismatch()
{
$end = new \DateTime('-1 hour');
$begin = new \DateTime('-10 hour');
$activity = new Activity();
$project1 = new Project();
$project2 = new Project();
$activity->setProject($project1);
$timesheet = new Timesheet();
$timesheet
->setBegin($begin)
->setEnd($end)
->setActivity($activity)
->setProject($project2)
;
$this->validator->validate($timesheet, new TimesheetConstraint(['message' => 'myMessage']));
$this->buildViolation('Project mismatch, project specific activity and timesheet project are different.')
->atPath('property.path.project')
->setCode(TimesheetConstraint::ACTIVITY_PROJECT_MISMATCH_ERROR)
->assertRaised();
}
public function testDisabledValuesDuringStart()
{
$begin = new \DateTime('-10 hour');
$customer = new Customer();
$customer->setVisible(false);
$activity = new Activity();
$activity->setVisible(false);
$project = new Project();
$project->setVisible(false);
$project->setCustomer($customer);
$activity->setProject($project);
$timesheet = new Timesheet();
$timesheet
->setBegin($begin)
->setActivity($activity)
->setProject($project)
;
$this->validator->validate($timesheet, new TimesheetConstraint(['message' => 'myMessage']));
$this->buildViolation('Cannot start a disabled activity.')
->atPath('property.path.activity')
->setCode(TimesheetConstraint::DISABLED_ACTIVITY_ERROR)
->buildNextViolation('Cannot start a disabled project.')
->atPath('property.path.project')
->setCode(TimesheetConstraint::DISABLED_PROJECT_ERROR)
->buildNextViolation('Cannot start a disabled customer.')
->atPath('property.path.customer')
->setCode(TimesheetConstraint::DISABLED_CUSTOMER_ERROR)
->assertRaised();
}
}

View File

@@ -72,6 +72,7 @@ class UserVoterTest extends AbstractVoterTest
yield [$user, $user4, 'edit', $result];
yield [$user, $user4, 'delete', $result];
yield [$user, $user4, 'hourly-rate', $result];
yield [$user, $user4, 'hourly-rate', $result];
}
$result = VoterInterface::ACCESS_ABSTAIN;