Release 2.39 (#5604)

* prepare audit via annotation
* default calendar slot label distance of 1h
+ replace freestyle config with dropdown
* added missing return definition in callbacks
* refactor view name handling
* dispatch calendar view changes and push them into the URL to be able to reload the poage
* bump packages
* fix timezone issue in calendar sum calculation
* fixes #5618 resetRates()
* show expected daily hours in working-contract screen
This commit is contained in:
Kevin Papst
2025-08-30 11:41:17 +02:00
committed by GitHub
parent 4920ea5075
commit a4d658b821
85 changed files with 987 additions and 516 deletions

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Audit;
use App\Audit\Loggable;
use App\Entity\CustomerMeta;
use PHPUnit\Framework\Attributes\CoversClass;
use Symfony\Component\Form\Test\TypeTestCase;
#[CoversClass(Loggable::class)]
class LoggableTest extends TypeTestCase
{
public function testConstruct(): void
{
$sut = new Loggable(CustomerMeta::class);
self::assertEquals(CustomerMeta::class, $sut->customFieldClass);
}
public function testHasAttributeAttributeOnLoggable(): void
{
$reflection = new \ReflectionClass(Loggable::class);
/** @var array<\ReflectionAttribute<\Attribute>> $attributes */
$attributes = array_filter($reflection->getAttributes(), fn ($attr) => $attr->getName() === \Attribute::class);
self::assertCount(1, $attributes, 'Loggable class should have the Attribute attribute');
$attribute = $attributes[0];
self::assertEquals(\Attribute::TARGET_CLASS, $attribute->getArguments()[0]);
}
}

View File

@@ -0,0 +1,34 @@
<?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\Audit;
use App\Audit\Versioned;
use PHPUnit\Framework\Attributes\CoversClass;
use Symfony\Component\Form\Test\TypeTestCase;
#[CoversClass(Versioned::class)]
class VersionedTest extends TypeTestCase
{
public function testConstruct(): void
{
$sut = new Versioned();
self::assertInstanceOf(Versioned::class, $sut);
}
public function testHasAttributeAttributeOnLoggable(): void
{
$reflection = new \ReflectionClass(Versioned::class);
/** @var array<\ReflectionAttribute<\Attribute>> $attributes */
$attributes = array_filter($reflection->getAttributes(), fn ($attr) => $attr->getName() === \Attribute::class);
self::assertCount(1, $attributes, 'Versioned class should have the Attribute attribute');
$attribute = $attributes[0];
self::assertEquals(\Attribute::TARGET_PROPERTY, $attribute->getArguments()[0]);
}
}

View File

@@ -0,0 +1,63 @@
<?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\Calendar;
use App\Calendar\CalendarQuery;
use App\Entity\User;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
#[CoversClass(CalendarQuery::class)]
class CalendarQueryTest extends TestCase
{
public function testConstruct(): void
{
$sut = new CalendarQuery();
self::assertNull($sut->getDate());
self::assertNull($sut->getUser());
self::assertEquals('month', $sut->getView());
$user = new User();
$sut->setUser($user);
self::assertSame($user, $sut->getUser());
$date = new \DateTimeImmutable('2025-08-13 12:13:14');
$sut->setDate($date);
self::assertNotNull($sut->getDate());
self::assertEquals('2025-08-13 12:13:14', $sut->getDate()->format('Y-m-d H:i:s'));
$sut->setView('foo');
self::assertEquals('month', $sut->getView());
}
#[DataProvider('getTestData')]
public function testSetView(string $value, string $expected): void
{
$sut = new CalendarQuery();
$sut->setView($value);
self::assertEquals($expected, $sut->getView());
}
/**
* @return iterable<int, array<int, string>>
*/
public static function getTestData(): iterable
{
yield ['agendaMonth', 'month'];
yield ['agendaWeek', 'week'];
yield ['agendaDay', 'day'];
yield ['month', 'month'];
yield ['week', 'week'];
yield ['day', 'day'];
yield ['foo', 'month'];
}
}

View File

@@ -193,7 +193,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
$fixture = new CustomerFixtures();
$fixture->setAmount(1);
$fixture->setCallback(function (Customer $customer) use ($invoiceTemplate) {
$fixture->setCallback(function (Customer $customer) use ($invoiceTemplate): void {
$customer->setInvoiceTemplate($invoiceTemplate[0]);
});
$customer = $this->importFixture($fixture)[0];

View File

@@ -68,7 +68,7 @@ class ActivityControllerTest extends AbstractControllerBaseTestCase
$fixture = new ActivityFixtures();
$fixture->setAmount(5);
$fixture->setCallback(function (Activity $activity) {
$fixture->setCallback(function (Activity $activity): void {
$activity->setVisible(true);
$activity->setComment('I am a foobar with tralalalala some more content');
$activity->setMetaField((new ActivityMeta())->setName('location')->setValue('homeoffice'));
@@ -111,7 +111,7 @@ class ActivityControllerTest extends AbstractControllerBaseTestCase
$fixture = new ActivityFixtures();
$fixture->setAmount(5);
$fixture->setCallback(function (Activity $activity) {
$fixture->setCallback(function (Activity $activity): void {
$activity->setVisible(true);
$activity->setComment('I am a foobar with tralalalala some more content');
$activity->setMetaField((new ActivityMeta())->setName('location')->setValue('homeoffice'));

View File

@@ -9,7 +9,11 @@
namespace App\Tests\Controller;
use App\DataFixtures\UserFixtures;
use App\Entity\User;
use App\Repository\UserRepository;
use App\WorkingTime\Calculator\WorkingTimeCalculatorDay;
use App\WorkingTime\Mode\WorkingTimeModeDay;
use PHPUnit\Framework\Attributes\Group;
#[Group('integration')]
@@ -31,6 +35,34 @@ class ContractControllerTest extends AbstractControllerBaseTestCase
self::assertEquals(0, $node->count());
}
public function testIndexActionWithWorkContract(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
/** @var UserRepository $repository */
$repository = $this->getPrivateService(UserRepository::class);
$user = $this->loadUserFromDatabase(UserFixtures::USERNAME_USER);
$user->setWorkContractMode(WorkingTimeModeDay::ID);
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_MONDAY, '28800');
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_TUESDAY, '28800');
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_WEDNESDAY, '28800');
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_THURSDAY, '25200');
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_FRIDAY, '19800');
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_SATURDAY, '0');
$user->setPreferenceValue(WorkingTimeCalculatorDay::WORK_HOURS_SUNDAY, '0');
$repository->saveUser($user);
$this->assertAccessIsGranted($client, '/contract');
$content = $client->getResponse()->getContent();
self::assertNotFalse($content);
$node = $client->getCrawler()->filter('table#working_times_details');
self::assertEquals(1, $node->count());
self::assertStringContainsString('7:00', $content);
self::assertStringContainsString('8:00', $content);
self::assertStringContainsString('5:30', $content);
}
public function testTeamleadCanChangeUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);

View File

@@ -68,7 +68,7 @@ class CustomerControllerTest extends AbstractControllerBaseTestCase
$fixture = new CustomerFixtures();
$fixture->setAmount(5);
$fixture->setCallback(function (Customer $customer) {
$fixture->setCallback(function (Customer $customer): void {
$customer->setVisible(true);
$customer->setComment('I am a foobar with tralalalala some more content');
$customer->setMetaField((new CustomerMeta())->setName('location')->setValue('homeoffice'));

View File

@@ -66,7 +66,7 @@ class ExportControllerTest extends AbstractControllerBaseTestCase
->setUser($user)
->setAmount(20)
->setStartDate($begin)
->setCallback(function (Timesheet $timesheet) use ($team, $em) {
->setCallback(function (Timesheet $timesheet) use ($team, $em): void {
$team->addProject($timesheet->getProject());
$em->persist($team);
})

View File

@@ -73,7 +73,7 @@ class ProjectControllerTest extends AbstractControllerBaseTestCase
$fixture = new ProjectFixtures();
$fixture->setAmount(5);
$i = 0;
$fixture->setCallback(function (Project $project) use (&$i) {
$fixture->setCallback(function (Project $project) use (&$i): void {
$project->setVisible(true);
switch ($i++) {
case 0:
@@ -137,7 +137,7 @@ class ProjectControllerTest extends AbstractControllerBaseTestCase
$fixture = new ProjectFixtures();
$fixture->setAmount(5);
$fixture->setCallback(function (Project $project) {
$fixture->setCallback(function (Project $project): void {
$project->setVisible(true);
$project->setComment('I am a foobar with tralalalala some more content');
$project->setMetaField((new ProjectMeta())->setName('location')->setValue('homeoffice'));

View File

@@ -46,7 +46,7 @@ class CustomerMonthlyProjectsControllerTest extends AbstractControllerBaseTestCa
$projects->setCustomers($customers);
$projects->setAmount(2);
$projects->setIsVisible(true);
$projects->setCallback(function (Project $project) {
$projects->setCallback(function (Project $project): void {
$project->setIsMonthlyBudget();
});
$this->importFixture($projects);

View File

@@ -40,7 +40,7 @@ class ProjectDateRangeControllerTest extends AbstractControllerBaseTestCase
$projects->setCustomers($customers);
$projects->setAmount(2);
$projects->setIsVisible(true);
$projects->setCallback(function (Project $project) {
$projects->setCallback(function (Project $project): void {
$project->setIsMonthlyBudget();
});
$this->importFixture($projects);

View File

@@ -49,7 +49,7 @@ class TeamControllerTest extends AbstractControllerBaseTestCase
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$fixture = new TeamFixtures();
$fixture->setAmount(5);
$fixture->setCallback(function (Team $team) {
$fixture->setCallback(function (Team $team): void {
$team->setName($team->getName() . '- fantastic team with foooo bar magic');
});
$this->importFixture($fixture);

View File

@@ -108,7 +108,7 @@ class TimesheetControllerTest extends AbstractControllerBaseTestCase
$fixture->setAmount(5);
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
$fixture->setStartDate($start);
$fixture->setCallback(function (Timesheet $timesheet) use ($tags) {
$fixture->setCallback(function (Timesheet $timesheet) use ($tags): void {
$timesheet->setDescription('I am a foobar with tralalalala some more content');
$timesheet->setMetaField((new TimesheetMeta())->setName('location')->setValue('homeoffice'));
$timesheet->setMetaField((new TimesheetMeta())->setName('feature')->setValue('timetracking'));
@@ -142,7 +142,7 @@ class TimesheetControllerTest extends AbstractControllerBaseTestCase
$fixture = new TimesheetFixtures();
$fixture->setAmount(15);
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
$fixture->setCallback(function (Timesheet $timesheet) {
$fixture->setCallback(function (Timesheet $timesheet): void {
$duration = rand(3600, 36000);
$begin = new \DateTime('-15 days');
$end = clone $begin;
@@ -442,7 +442,7 @@ class TimesheetControllerTest extends AbstractControllerBaseTestCase
$end = new \DateTime('2018-08-02T20:30:00');
$fixture = new TimesheetFixtures();
$fixture->setCallback(function (Timesheet $timesheet) use ($begin, $end) {
$fixture->setCallback(function (Timesheet $timesheet) use ($begin, $end): void {
$timesheet->setBegin($begin);
$timesheet->setEnd($end);
});
@@ -474,7 +474,7 @@ class TimesheetControllerTest extends AbstractControllerBaseTestCase
$fixture->setAmount(1);
$fixture->setIsGlobal(true);
$fixture->setIsVisible(true);
$fixture->setCallback(function (Activity $activity) {
$fixture->setCallback(function (Activity $activity): void {
$activity->setBudget(1000);
$activity->setTimeBudget(3600);
});
@@ -523,7 +523,7 @@ class TimesheetControllerTest extends AbstractControllerBaseTestCase
$fixture->setAmount(1);
$fixture->setIsGlobal(true);
$fixture->setIsVisible(true);
$fixture->setCallback(function (Activity $activity) {
$fixture->setCallback(function (Activity $activity): void {
$activity->setBudget(1000);
$activity->setTimeBudget(3600);
});
@@ -813,7 +813,7 @@ class TimesheetControllerTest extends AbstractControllerBaseTestCase
$fixture->setAmountRunning(0);
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
$fixture->setStartDate($dateTime->createDateTime());
$fixture->setCallback(function (Timesheet $timesheet) {
$fixture->setCallback(function (Timesheet $timesheet): void {
$timesheet->setDescription('Testing is fun!');
$begin = clone $timesheet->getBegin();
$begin->setTime(0, 0, 0);

View File

@@ -97,7 +97,7 @@ class TimesheetTeamControllerTest extends AbstractControllerBaseTestCase
$fixture->setAmount(5);
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
$fixture->setStartDate($start);
$fixture->setCallback(function (Timesheet $timesheet) {
$fixture->setCallback(function (Timesheet $timesheet): void {
$timesheet->setDescription('I am a foobar with tralalalala some more content');
$timesheet->setMetaField((new TimesheetMeta())->setName('location')->setValue('homeoffice'));
$timesheet->setMetaField((new TimesheetMeta())->setName('feature')->setValue('timetracking'));
@@ -428,7 +428,7 @@ class TimesheetTeamControllerTest extends AbstractControllerBaseTestCase
$fixture->setAmountRunning(0);
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
$fixture->setStartDate($dateTime->createDateTime());
$fixture->setCallback(function (Timesheet $timesheet) {
$fixture->setCallback(function (Timesheet $timesheet): void {
$timesheet->setDescription('Testing is fun!');
$begin = clone $timesheet->getBegin();
$begin->setTime(0, 0, 0);

View File

@@ -22,12 +22,15 @@ class ArrayFormatterTest extends AbstractFormatterTestCase
return new ArrayFormatter();
}
protected function getActualValue()
/**
* @return string[]
*/
protected function getActualValue(): array
{
return ['test', 'foo', 'bar'];
}
protected function getExpectedValue()
protected function getExpectedValue(): string
{
return 'test;foo;bar';
}

View File

@@ -22,12 +22,12 @@ class BooleanFormatterTest extends AbstractFormatterTestCase
return new BooleanFormatter();
}
protected function getActualValue()
protected function getActualValue(): bool
{
return false;
}
protected function getExpectedValue()
protected function getExpectedValue(): bool
{
return false;
}

View File

@@ -27,12 +27,12 @@ class DateFormatterTest extends AbstractFormatterTestCase
return new DateFormatter();
}
protected function getActualValue()
protected function getActualValue(): \DateTimeInterface
{
return $this->date = new \DateTime();
}
protected function getExpectedValue()
protected function getExpectedValue(): bool|float
{
return Date::PHPToExcel($this->date);
}

View File

@@ -24,12 +24,12 @@ class DurationFormatterTest extends AbstractFormatterTestCase
return new DurationFormatter();
}
protected function getActualValue()
protected function getActualValue(): int
{
return 3600;
}
protected function getExpectedValue()
protected function getExpectedValue(): string
{
return '=3600/86400';
}

View File

@@ -24,7 +24,7 @@ class TimesheetExportRepositoryTest extends TestCase
public function testSetExported(): void
{
$repository = $this->createMock(TimesheetRepository::class);
$repository->expects($this->once())->method('setExported')->willReturnCallback(function (array $items) {
$repository->expects($this->once())->method('setExported')->willReturnCallback(function (array $items): void {
self::assertCount(2, $items);
});

View File

@@ -0,0 +1,51 @@
<?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\Form\Type;
use App\Form\Type\CalendarViewType;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Test\TypeTestCase;
#[CoversClass(CalendarViewType::class)]
class CalendarViewTypeTest extends TypeTestCase
{
/**
* @return iterable<int, array<int, string>>
*/
public static function getTestData(): iterable
{
yield ['month', 'month'];
yield ['week', 'week'];
yield ['day', 'day'];
}
#[DataProvider('getTestData')]
public function testSubmitValidData(string $value, string $expected): void
{
$data = ['view' => $value];
$model = new TypeTestModel(['view' => 'some']);
$form = $this->factory->createBuilder(FormType::class, $model);
$form->add('view', CalendarViewType::class);
$form = $form->getForm();
$expected = new TypeTestModel([
'view' => $expected
]);
dump($data);
$form->submit($data);
self::assertTrue($form->isSynchronized());
self::assertEquals($expected, $model);
}
}

View File

@@ -24,7 +24,7 @@ class TimesheetInvoiceItemRepositoryTest extends TestCase
public function testSetExported(): void
{
$repository = $this->createMock(TimesheetRepository::class);
$repository->expects($this->once())->method('setExported')->willReturnCallback(function (array $items) {
$repository->expects($this->once())->method('setExported')->willReturnCallback(function (array $items): void {
self::assertCount(2, $items);
});

View File

@@ -61,7 +61,7 @@ class SamlLogoutSubscriberTest extends TestCase
$auth = $this->getMockBuilder(Auth::class)->disableOriginalConstructor()->getMock();
$auth->expects($this->once())->method('processSLO')->willThrowException(new Error('blub'));
$auth->expects($this->once())->method('getSLOurl')->willReturn('/logout');
$auth->expects($this->once())->method('logout')->willReturnCallback(function () {
$auth->expects($this->once())->method('logout')->willReturnCallback(function (): void {
$args = \func_get_args();
self::assertNull($args[0]);
self::assertEquals([], $args[1]);

View File

@@ -1301,36 +1301,6 @@ parameters:
count: 1
path: Export/Spreadsheet/CellFormatter/AbstractFormatterTestCase.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\ArrayFormatterTest\\:\\:getActualValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/ArrayFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\ArrayFormatterTest\\:\\:getExpectedValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/ArrayFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\BooleanFormatterTest\\:\\:getActualValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/BooleanFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\BooleanFormatterTest\\:\\:getExpectedValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/BooleanFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\DateFormatterTest\\:\\:getActualValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/DateFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\DateFormatterTest\\:\\:getExpectedValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/DateFormatterTest.php
-
message: "#^Property App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\DateFormatterTest\\:\\:\\$date has no type specified\\.$#"
count: 1
@@ -1351,16 +1321,6 @@ parameters:
count: 1
path: Export/Spreadsheet/CellFormatter/DateTimeFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\DurationFormatterTest\\:\\:getActualValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/DurationFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\DurationFormatterTest\\:\\:getExpectedValue\\(\\) has no return type specified\\.$#"
count: 1
path: Export/Spreadsheet/CellFormatter/DurationFormatterTest.php
-
message: "#^Method App\\\\Tests\\\\Export\\\\Spreadsheet\\\\CellFormatter\\\\TimeFormatterTest\\:\\:getActualValue\\(\\) has no return type specified\\.$#"
count: 1