Release 2.32 (#5411)

* bump packages
* dynamic invoice options
* make sure that invoice previews can be detected
* support for mpdf associated files
* do not include any future times in work contract calculation
* re-add username column in Excel spreadsheet
* deactivate internal rate editing
* show if plugin update exists
* shorten name to Kimai only, without Time-Tracking
* remove check for existing id in work contract
* fix metafield already defined in search
* helper methods to unlock months
* new translation
* send event on unlock month
This commit is contained in:
Kevin Papst
2025-04-06 09:53:48 +02:00
committed by GitHub
parent 2a75cd6230
commit 2e6b700b43
58 changed files with 749 additions and 437 deletions

View File

@@ -51,7 +51,7 @@ class PasswordResetControllerTest extends AbstractControllerBaseTestCase
$content = $response->getContent();
self::assertNotFalse($content);
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
self::assertStringContainsString('<title>Kimai</title>', $content);
self::assertStringContainsString('Reset your password', $content);
self::assertStringContainsString('<form class="card-body security-password-reset" action="/en/resetting/send-email" method="post" autocomplete="off">', $content);
self::assertStringContainsString('<input autocomplete="username" type="text"', $content);

View File

@@ -45,7 +45,7 @@ class SecurityControllerTest extends AbstractControllerBaseTestCase
self::assertTrue($client->getResponse()->isSuccessful());
$content = $response->getContent();
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
self::assertStringContainsString('<title>Kimai</title>', $content);
self::assertStringContainsString('<form action="/en/login_check" method="post"', $content);
self::assertStringContainsString('<input autocomplete="username" type="text" id="username" name="_username"', $content);
self::assertStringContainsString('<input autocomplete="new-password" id="password" name="_password" type="password"', $content);

View File

@@ -56,7 +56,7 @@ class SelfRegistrationControllerTest extends AbstractControllerBaseTestCase
self::assertTrue($response->isSuccessful());
$content = $response->getContent();
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
self::assertStringContainsString('<title>Kimai</title>', $content);
self::assertStringContainsString('Register a new account', $content);
self::assertStringContainsString('<form name="user_registration_form" method="post" action="/en/register/"', $content);
self::assertStringContainsString('<input type="email"', $content);
@@ -115,7 +115,7 @@ class SelfRegistrationControllerTest extends AbstractControllerBaseTestCase
$this->createUser($client, 'example', 'register@example.com', 'test1234');
$content = $client->getResponse()->getContent();
self::assertStringContainsString('<title>Kimai – Time Tracking</title>', $content);
self::assertStringContainsString('<title>Kimai</title>', $content);
self::assertStringContainsString('An e-mail has been sent to register@example.com. It contains a link you must click to activate your account.', $content);
self::assertStringContainsString('<a href="/en/login">', $content);
}

View File

@@ -21,14 +21,14 @@ class ThemeJavascriptTranslationsEventTest extends TestCase
public function testDefaultValues(): void
{
$sut = new ThemeJavascriptTranslationsEvent();
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
self::assertCount(self::COUNTER, $sut->getTranslations());
}
public function testGetterAndSetter(): void
{
$sut = new ThemeJavascriptTranslationsEvent();
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
$sut->setTranslation('foo', 'bar');
$sut->setTranslation('hello', 'world', 'testing');

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\Event;
use App\Entity\User;
use App\Event\WorkingTimeApproveMonthEvent;
use App\WorkingTime\Model\Month;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Event\WorkingTimeApproveMonthEvent
*/
class WorkingTimeApproveMonthEventTest extends TestCase
{
public function testGetter(): void
{
$user = new User();
$approver = new User();
$approvalMonth = new Month(new \DateTimeImmutable('2023-02-10'), $user);
$sut = new WorkingTimeApproveMonthEvent($approvalMonth, $approver);
self::assertSame($user, $sut->getUser()); // @phpstan-ignore method.deprecated
self::assertSame($approvalMonth, $sut->getMonth());
self::assertSame($approver, $sut->getApprovedBy());
}
}

View File

@@ -0,0 +1,33 @@
<?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\Event;
use App\Entity\User;
use App\Event\WorkingTimeUnlockMonthEvent;
use App\WorkingTime\Model\Month;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Event\WorkingTimeUnlockMonthEvent
*/
class WorkingTimeUnlockMonthEventTest extends TestCase
{
public function testGetter(): void
{
$user = new User();
$approver = new User();
$approvalMonth = new Month(new \DateTimeImmutable('2023-02-10'), $user);
$sut = new WorkingTimeUnlockMonthEvent($approvalMonth, $approver);
self::assertSame($approvalMonth, $sut->getMonth());
self::assertSame($approver, $sut->getUnlockedBy());
}
}

View File

@@ -22,7 +22,7 @@ abstract class AbstractActionsSubscriberTestCase extends TestCase
protected function createSubscriber(string $className, ...$grants): AbstractActionsSubscriber
{
$auth = $this->createMock(AuthorizationCheckerInterface::class);
$auth->method('isGranted')->willReturnOnConsecutiveCalls(...$grants);
$auth->method('isGranted')->willReturnOnConsecutiveCalls(...$grants); // @phpstan-ignore argument.named
$router = $this->createMock(UrlGeneratorInterface::class);
$router->method('generate')->willReturnArgument(0);

View File

@@ -27,6 +27,7 @@ class EmailSubscriberTest extends TestCase
$events = EmailSubscriber::getSubscribedEvents();
self::assertArrayHasKey(EmailEvent::class, $events);
$methodName = $events[EmailEvent::class][0];
self::assertIsString($methodName);
self::assertTrue(method_exists(EmailSubscriber::class, $methodName));
}

View File

@@ -31,10 +31,12 @@ class LastLoginSubscriberTest extends TestCase
self::assertArrayHasKey(UserInteractiveLoginEvent::class, $events);
$methodName = $events[UserInteractiveLoginEvent::class];
self::assertIsString($methodName);
self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName));
self::assertArrayHasKey(LoginSuccessEvent::class, $events);
$methodName = $events[LoginSuccessEvent::class];
self::assertIsString($methodName);
self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName));
}

View File

@@ -23,6 +23,7 @@ class MenuSubscriberTest extends TestCase
$events = MenuSubscriber::getSubscribedEvents();
self::assertArrayHasKey(ConfigureMainMenuEvent::class, $events);
$methodName = $events[ConfigureMainMenuEvent::class][0];
self::assertIsString($methodName);
self::assertTrue(method_exists(MenuSubscriber::class, $methodName));
}
}

View File

@@ -29,6 +29,7 @@ class PagerfantaExceptionSubscriberTest extends TestCase
$events = PagerfantaExceptionSubscriber::getSubscribedEvents();
self::assertArrayHasKey(KernelEvents::EXCEPTION, $events);
$methodName = $events[KernelEvents::EXCEPTION][0];
self::assertIsString($methodName);
self::assertTrue(method_exists(PagerfantaExceptionSubscriber::class, $methodName));
}

View File

@@ -33,6 +33,7 @@ class ProfileSubscriberTest extends TestCase
self::assertArrayHasKey(LoginSuccessEvent::class, $events);
$methodName = $events[LoginSuccessEvent::class];
self::assertIsString($methodName);
self::assertTrue(method_exists(LastLoginSubscriber::class, $methodName));
}

View File

@@ -23,6 +23,7 @@ class UserDetailsSubscriberTest extends TestCase
$events = UserDetailsSubscriber::getSubscribedEvents();
self::assertArrayHasKey(UserDetailsEvent::class, $events);
$methodName = $events[UserDetailsEvent::class][0];
self::assertIsString($methodName);
self::assertTrue(method_exists(UserDetailsSubscriber::class, $methodName));
}
}

View File

@@ -44,6 +44,7 @@ class UserPreferenceSubscriberTest extends TestCase
$events = UserPreferenceSubscriber::getSubscribedEvents();
self::assertArrayHasKey(PrepareUserEvent::class, $events);
$methodName = $events[PrepareUserEvent::class][0];
self::assertIsString($methodName);
self::assertTrue(method_exists(UserPreferenceSubscriber::class, $methodName));
}

View File

@@ -23,6 +23,7 @@ class WizardSubscriberTest extends TestCase
$events = WizardSubscriber::getSubscribedEvents();
self::assertArrayHasKey(KernelEvents::REQUEST, $events);
$methodName = $events[KernelEvents::REQUEST][0];
self::assertIsString($methodName);
self::assertTrue(method_exists(WizardSubscriber::class, $methodName));
}
}

View File

@@ -109,6 +109,7 @@ class CsvRendererTest extends AbstractRendererTestCase
'0',
'0',
'84',
'Kevin',
'kevin',
'',
'Customer Name',
@@ -142,6 +143,7 @@ class CsvRendererTest extends AbstractRendererTestCase
'0',
'0',
'-100.92',
'niveK',
'nivek',
'',
'Customer Name',
@@ -168,6 +170,6 @@ class CsvRendererTest extends AbstractRendererTestCase
self::assertEquals($expected, $all[5]);
self::assertEquals($expected2, $all[6]);
self::assertEquals(\count($expected), \count($all[0]));
self::assertEquals('foo', $all[4][16]);
self::assertEquals('foo', $all[4][17]);
}
}

View File

@@ -52,17 +52,19 @@ abstract class AbstractRendererTestCase extends KernelTestCase
$activity->setProject($project);
$activity->setMetaField((new ActivityMeta())->setName('activity-foo')->setValue('activity-bar')->setIsVisible(true));
$userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier'];
$userMethods = ['getId', 'getPreferenceValue', 'getUsername', 'getUserIdentifier', 'getAlias'];
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');
$user1->method('getAlias')->willReturn('Foo Bar');
$user1->method('getUserIdentifier')->willReturn('foo-bar');
$user1->method('getUsername')->willReturn('foo-bar');
$user2 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user2->method('getId')->willReturn(2);
$user2->method('getUsername')->willReturn('hello-world');
$user2->method('getAlias')->willReturn('Hello World');
$user2->method('getUserIdentifier')->willReturn('hello-world');
$user2->method('getUsername')->willReturn('hello-world');
$timesheet = new Timesheet();
$timesheet
@@ -111,6 +113,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase
;
$userKevin = new User();
$userKevin->setAlias('Kevin');
$userKevin->setUserIdentifier('kevin');
$timesheet5 = new Timesheet();
@@ -129,6 +132,7 @@ abstract class AbstractRendererTestCase extends KernelTestCase
;
$userNivek = new User();
$userNivek->setAlias('niveK');
$userNivek->setUserIdentifier('nivek');
$timesheet6 = new Timesheet();

View File

@@ -32,6 +32,8 @@ class InvoiceModelTest extends TestCase
self::assertNotNull($sut->getCustomer());
self::assertNotNull($sut->getTemplate());
self::assertFalse($sut->isPreview());
self::assertFalse($sut->isHideZeroTax());
self::assertNull($sut->getCalculator());
self::assertEmpty($sut->getEntries());
self::assertIsArray($sut->getEntries());
@@ -43,6 +45,12 @@ class InvoiceModelTest extends TestCase
$sut->setFormatter($newFormatter);
self::assertNotSame($formatter, $sut->getFormatter());
self::assertSame($newFormatter, $sut->getFormatter());
self::assertEquals([], $sut->getOptions());
$sut->setPreview(true);
$sut->setHideZeroTax(true);
self::assertTrue($sut->isPreview());
self::assertTrue($sut->isHideZeroTax());
}
public function testEmptyObjectThrowsExceptionOnNumberGenerator(): void
@@ -75,6 +83,23 @@ class InvoiceModelTest extends TestCase
self::assertSame($template, $sut->getTemplate());
self::assertInstanceOf(\DateTimeInterface::class, $sut->getDueDate());
$sut->setOption('foo-int', 123);
$sut->setOption('foo-float', 123.45);
$sut->setOption('foo-string', '12345');
$sut->setOption('foo-null', null);
$sut->setOption('foo-bool', true);
$sut->setOption('foo-array', ['foo' => 'bar']);
$sut->setOption('foo-array2', ['foo', 'bar']);
self::assertEquals([
'foo-int' => 123,
'foo-float' => 123.45,
'foo-string' => '12345',
'foo-null' => null,
'foo-bool' => true,
'foo-array' => ['foo' => 'bar'],
'foo-array2' => ['foo', 'bar']
], $sut->getOptions());
}
public function testDueDate(): void

View File

@@ -50,7 +50,7 @@ class ThemeEventExtensionTest extends TestCase
];
}
protected function getSut(bool $hasListener = true, string $title = null): ThemeExtension
protected function getSut(bool $hasListener = true): ThemeExtension
{
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->method('hasListeners')->willReturn($hasListener);
@@ -155,18 +155,6 @@ class ThemeEventExtensionTest extends TestCase
public function testGetTitle(): void
{
$sut = $this->getSut(false);
self::assertEquals('Kimai – foo', $sut->generateTitle());
self::assertEquals('sdfsdf | Kimai – foo', $sut->generateTitle('sdfsdf | '));
self::assertEquals('<b>Kimai</b> ... foo', $sut->generateTitle('<b>', '</b> ... '));
self::assertEquals('Kimai | foo', $sut->generateTitle(null, ' | '));
}
public function testGetBrandedTitle(): void
{
$sut = $this->getSut(false, 'MyCompany');
self::assertEquals('Kimai – foo', $sut->generateTitle());
self::assertEquals('sdfsdf | Kimai – foo', $sut->generateTitle('sdfsdf | '));
self::assertEquals('<b>Kimai</b> ... foo', $sut->generateTitle('<b>', '</b> ... '));
self::assertEquals('Kimai | foo', $sut->generateTitle(null, ' | '));
self::assertEquals('Kimai', $sut->generateTitle());
}
}

View File

@@ -1311,51 +1311,11 @@ parameters:
count: 1
path: EventSubscriber/Actions/AbstractActionsSubscriberTestCase.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
count: 1
path: EventSubscriber/EmailSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, array\\<int\\|string, int\\|string\\>\\|int\\|string\\>\\|string given\\.$#"
count: 2
path: EventSubscriber/LastLoginSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
count: 1
path: EventSubscriber/MenuSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
count: 1
path: EventSubscriber/PagerfantaExceptionSubscriberTest.php
-
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\ProfileSubscriberTest\\:\\:getInvalidCookies\\(\\) has no return type specified\\.$#"
count: 1
path: EventSubscriber/ProfileSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, array\\<int\\|string, int\\|string\\>\\|int\\|string\\>\\|string given\\.$#"
count: 1
path: EventSubscriber/ProfileSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
count: 1
path: EventSubscriber/UserDetailsSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
count: 1
path: EventSubscriber/UserPreferenceSubscriberTest.php
-
message: "#^Parameter \\#2 \\$method of function method_exists expects string, array\\<int\\|string, int\\|string\\>\\|int\\|string given\\.$#"
count: 1
path: EventSubscriber/WizardSubscriberTest.php
-
message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|false given\\.$#"
count: 6