Release 2.7.0 (#4506)

* added api URL for simpler integration
* allow to request password change upon next login
* make ModifiedAt timesheet independent
* improved plugin api
* replace kernel calls with AutoconfigureTag and TaggedIterator attributes
* new translation keys (e.g. for days)
* support setting min and max date on date and daterange pickers
This commit is contained in:
Kevin Papst
2023-12-26 14:49:11 +01:00
committed by GitHub
parent f86eca05ae
commit ad645f5b58
72 changed files with 378 additions and 735 deletions

View File

@@ -17,22 +17,22 @@ use App\Entity\User;
*/
class StatusControllerTest extends APIControllerBaseTest
{
public function testIsSecurePing()
public function testIsSecurePing(): void
{
$this->assertUrlIsSecured('/api/ping');
}
public function testIsSecureVersion()
public function testIsSecureVersion(): void
{
$this->assertUrlIsSecured('/api/version');
}
public function testIsSecurePlugins()
public function testIsSecurePlugins(): void
{
$this->assertUrlIsSecured('/api/plugins');
}
public function testPing()
public function testPing(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/ping');
@@ -42,7 +42,7 @@ class StatusControllerTest extends APIControllerBaseTest
$this->assertEquals(['message' => 'pong'], $result);
}
public function testVersion()
public function testVersion(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/version');
@@ -62,12 +62,13 @@ class StatusControllerTest extends APIControllerBaseTest
);
}
public function testPlugins()
public function testPlugins(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/api/plugins');
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
// no asserts, as plugins are disabled in tests
}
}

View File

@@ -24,10 +24,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class ActivateUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -41,7 +38,7 @@ class ActivateUserCommandTest extends KernelTestCase
$this->application->add(new ActivateUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -49,7 +46,7 @@ class ActivateUserCommandTest extends KernelTestCase
self::assertInstanceOf(ActivateUserCommand::class, $command);
}
protected function callCommand(?string $username)
private function callCommand(?string $username): CommandTester
{
$command = $this->application->find('kimai:user:activate');
$input = [
@@ -66,7 +63,7 @@ class ActivateUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testActivate()
public function testActivate(): void
{
$commandTester = $this->callCommand('chris_user');
@@ -81,7 +78,7 @@ class ActivateUserCommandTest extends KernelTestCase
self::assertTrue($user->isEnabled());
}
public function testActivateOnActiveUser()
public function testActivateOnActiveUser(): void
{
$commandTester = $this->callCommand('susan_super');
@@ -89,7 +86,7 @@ class ActivateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "susan_super" is already active.', $output);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -30,7 +30,7 @@ class BundleInstallerCommandTest extends KernelTestCase
/**
* @param class-string $className
*/
protected function getCommand(string $className): Command
private function getCommand(string $className): Command
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);

View File

@@ -48,7 +48,7 @@ class ChangePasswordCommandTest extends KernelTestCase
self::assertInstanceOf(ChangePasswordCommand::class, $command);
}
protected function callCommand(?string $username, ?string $password): CommandTester
private function callCommand(?string $username, ?string $password): CommandTester
{
$command = $this->application->find('kimai:user:password');
$input = [

View File

@@ -24,10 +24,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class DeactivateUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -41,7 +38,7 @@ class DeactivateUserCommandTest extends KernelTestCase
$this->application->add(new DeactivateUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -49,7 +46,7 @@ class DeactivateUserCommandTest extends KernelTestCase
self::assertInstanceOf(DeactivateUserCommand::class, $command);
}
protected function callCommand(?string $username)
protected function callCommand(?string $username): CommandTester
{
$command = $this->application->find('kimai:user:deactivate');
$input = [
@@ -66,7 +63,7 @@ class DeactivateUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testDeactivate()
public function testDeactivate(): void
{
$commandTester = $this->callCommand('john_user');
@@ -81,7 +78,7 @@ class DeactivateUserCommandTest extends KernelTestCase
self::assertFalse($user->isEnabled());
}
public function testDeactivateOnDeactivatedUser()
public function testDeactivateOnDeactivatedUser(): void
{
$commandTester = $this->callCommand('chris_user');
@@ -89,7 +86,7 @@ class DeactivateUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "chris_user" is already deactivated.', $output);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -25,10 +25,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class DemoteUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -42,7 +39,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->application->add(new DemoteUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -50,7 +47,7 @@ class DemoteUserCommandTest extends KernelTestCase
self::assertInstanceOf(DemoteUserCommand::class, $command);
}
protected function callCommand(?string $username, ?string $role, bool $super = false)
private function callCommand(?string $username, ?string $role, bool $super = false): CommandTester
{
$command = $this->application->find('kimai:user:demote');
$input = [
@@ -75,7 +72,7 @@ class DemoteUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testDemoteRole()
public function testDemoteRole(): void
{
$commandTester = $this->callCommand('tony_teamlead', 'ROLE_TEAMLEAD');
@@ -90,7 +87,7 @@ class DemoteUserCommandTest extends KernelTestCase
self::assertFalse($user->hasTeamleadRole());
}
public function testDemoteSuper()
public function testDemoteSuper(): void
{
$commandTester = $this->callCommand('susan_super', null, true);
@@ -105,7 +102,7 @@ class DemoteUserCommandTest extends KernelTestCase
self::assertFalse($user->isSuperAdmin());
}
public function testDemoteSuperFailsOnTeamlead()
public function testDemoteSuperFailsOnTeamlead(): void
{
$commandTester = $this->callCommand('tony_teamlead', null, true);
@@ -113,7 +110,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "tony_teamlead" doesn\'t have the super administrator role.', $output);
}
public function testDemoteAdminFailsOnTeamlead()
public function testDemoteAdminFailsOnTeamlead(): void
{
$commandTester = $this->callCommand('tony_teamlead', 'ROLE_ADMIN', false);
@@ -121,7 +118,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "tony_teamlead" didn\'t have "ROLE_ADMIN" role.', $output);
}
public function testDemoteRoleAndSuperFails()
public function testDemoteRoleAndSuperFails(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('You can pass either the role or the --super option (but not both simultaneously).');
@@ -129,7 +126,7 @@ class DemoteUserCommandTest extends KernelTestCase
$this->callCommand('john_user', 'ROLE_TEAMLEAD', true);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -37,9 +37,9 @@ class ExportCreateCommandTest extends KernelTestCase
{
use KernelTestTrait;
protected Application $application;
private Application $application;
private function clearExportFiles()
private function clearExportFiles(): void
{
$path = __DIR__ . '/../_data/export/';
@@ -103,7 +103,7 @@ class ExportCreateCommandTest extends KernelTestCase
* @param array $options
* @return CommandTester
*/
protected function createExport(array $options = [])
protected function createExport(array $options = []): CommandTester
{
$command = $this->application->find('kimai:export:create');
$commandTester = new CommandTester($command);
@@ -114,7 +114,7 @@ class ExportCreateCommandTest extends KernelTestCase
return $commandTester;
}
protected function assertCommandErrors(array $options = [], string $errorMessage = '')
protected function assertCommandErrors(array $options = [], string $errorMessage = ''): void
{
$commandTester = $this->createExport($options);
@@ -122,7 +122,7 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('[ERROR] ' . $errorMessage, $output);
}
protected function assertCommandResult(array $options = [], string $message = '')
protected function assertCommandResult(array $options = [], string $message = ''): void
{
$commandTester = $this->createExport($options);
@@ -130,47 +130,47 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] ' . $message, $output);
}
public function testCreateWithUnknownExportFilter()
public function testCreateWithUnknownExportFilter(): void
{
$this->assertCommandErrors(['--exported' => 'foo'], 'Unknown "exported" filter given');
}
public function testCreateWithUnknownTemplate()
public function testCreateWithUnknownTemplate(): void
{
$this->assertCommandErrors(['--template' => 'foo'], 'Unknown export "template", available are:');
}
public function testCreateWithMissingTemplate()
public function testCreateWithMissingTemplate(): void
{
$this->assertCommandErrors([], 'You must pass the "template" option');
}
public function testCreateWithInvalidStart()
public function testCreateWithInvalidStart(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--start' => '202ß-ä1-01'], 'Invalid start date given');
}
public function testCreateWithInvalidEnd()
public function testCreateWithInvalidEnd(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--end' => '202ß-ä1-01'], 'Invalid end date given');
}
public function testCreateWithInvalidDirectory()
public function testCreateWithInvalidDirectory(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--directory' => '/tzuikmnbgtz/'], 'Invalid "directory" given: /tzuikmnbgtz/');
}
public function testCreateWithInvalidEmail()
public function testCreateWithInvalidEmail(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--email' => ['tzuikmnbgtz']], 'Invalid "email" given: tzuikmnbgtz');
}
public function testCreateWithInvalidEmails()
public function testCreateWithInvalidEmails(): void
{
$this->assertCommandErrors(['--template' => 'csv', '--email' => ['foo@example.com', 'foo@1']], 'Invalid "email" given: foo@1');
}
public function testCreateWithMissingEntries()
public function testCreateWithMissingEntries(): void
{
$options = ['--set-exported' => null, '--customer' => [1], '--template' => 'csv', '--start' => '2020-01-01', '--end' => '2020-03-01'];
$commandTester = $this->createExport($options);
@@ -179,7 +179,11 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('[OK] No entries found, skipping', $output);
}
protected function prepareFixtures(\DateTime $start)
/**
* @param \DateTime $start
* @return array{0: Customer, 1: array<Project>}
*/
private function prepareFixtures(\DateTime $start): array
{
$fixture = new CustomerFixtures();
$fixture->setAmount(1);
@@ -200,7 +204,7 @@ class ExportCreateCommandTest extends KernelTestCase
return [$customer, $project];
}
public function testCreateExportByCustomer()
public function testCreateExportByCustomer(): void
{
$start = new \DateTime('-2 months');
$end = new \DateTime();
@@ -213,7 +217,7 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('Saved export to: ', $output);
}
public function testCreateExportByProject()
public function testCreateExportByProject(): void
{
$start = new \DateTime('-2 months');
$end = new \DateTime();
@@ -226,7 +230,7 @@ class ExportCreateCommandTest extends KernelTestCase
$this->assertStringContainsString('Saved export to: ', $output);
}
public function testCreateExportWithEmail()
public function testCreateExportWithEmail(): void
{
$start = new \DateTime('-2 months');
$end = new \DateTime();

View File

@@ -19,10 +19,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
*/
class InstallCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function setUp(): void
{
@@ -37,7 +34,7 @@ class InstallCommandTest extends KernelTestCase
));
}
public function testCommandName()
public function testCommandName(): void
{
$command = $this->application->find('kimai:install');
self::assertInstanceOf(InstallCommand::class, $command);

View File

@@ -35,7 +35,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
{
use KernelTestTrait;
protected Application $application;
private Application $application;
private function clearInvoiceFiles(): void
{

View File

@@ -22,16 +22,13 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class PluginCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
public function testWithPlugins()
public function testWithPlugins(): void
{
$plugin1 = $this->getMockBuilder(PluginInterface::class)->onlyMethods(['getName', 'getPath'])->getMock();
$plugin1->expects($this->any())->method('getName')->willReturn('TestBundle');
$plugin1->expects($this->once())->method('getPath')->willReturn(__DIR__ . '/../Plugin/Fixtures/TestPlugin');
$plugin1->expects($this->exactly(2))->method('getPath')->willReturn(__DIR__ . '/../Plugin/Fixtures/TestPlugin');
$commandTester = $this->getCommandTester([$plugin1], []);
$output = $commandTester->getDisplay();
@@ -40,7 +37,7 @@ class PluginCommandTest extends KernelTestCase
$this->assertStringContainsString('TestPlugin from composer.json', $output);
}
protected function getCommandTester(array $plugins, array $options = [])
private function getCommandTester(array $plugins, array $options = []): CommandTester
{
$kernel = self::bootKernel();
$this->application = new Application($kernel);

View File

@@ -25,10 +25,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class PromoteUserCommandTest extends KernelTestCase
{
/**
* @var Application
*/
private $application;
private Application $application;
protected function setUp(): void
{
@@ -42,7 +39,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->application->add(new PromoteUserCommand($userService));
}
public function testCommandName()
public function testCommandName(): void
{
$application = $this->application;
@@ -50,7 +47,7 @@ class PromoteUserCommandTest extends KernelTestCase
self::assertInstanceOf(PromoteUserCommand::class, $command);
}
protected function callCommand(?string $username, ?string $role, bool $super = false)
protected function callCommand(?string $username, ?string $role, bool $super = false): CommandTester
{
$command = $this->application->find('kimai:user:promote');
$input = [
@@ -75,7 +72,7 @@ class PromoteUserCommandTest extends KernelTestCase
return $commandTester;
}
public function testPromoteRole()
public function testPromoteRole(): void
{
$commandTester = $this->callCommand('john_user', 'ROLE_TEAMLEAD');
@@ -90,7 +87,7 @@ class PromoteUserCommandTest extends KernelTestCase
self::assertTrue($user->hasTeamleadRole());
}
public function testPromoteSuper()
public function testPromoteSuper(): void
{
$commandTester = $this->callCommand('john_user', null, true);
@@ -105,7 +102,7 @@ class PromoteUserCommandTest extends KernelTestCase
self::assertTrue($user->isSuperAdmin());
}
public function testPromoteSuperFailsOnSuperAdmin()
public function testPromoteSuperFailsOnSuperAdmin(): void
{
$commandTester = $this->callCommand('susan_super', null, true);
@@ -113,7 +110,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "susan_super" does already have the super administrator role.', $output);
}
public function testPromoteTeamleadFailsOnTeamlead()
public function testPromoteTeamleadFailsOnTeamlead(): void
{
$commandTester = $this->callCommand('tony_teamlead', 'ROLE_TEAMLEAD', false);
@@ -121,7 +118,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->assertStringContainsString('[WARNING] User "tony_teamlead" did already have "ROLE_TEAMLEAD" role.', $output);
}
public function testPromoteRoleAndSuperFails()
public function testPromoteRoleAndSuperFails(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('You can pass either the role or the --super option (but not both simultaneously).');
@@ -129,7 +126,7 @@ class PromoteUserCommandTest extends KernelTestCase
$this->callCommand('john_user', 'ROLE_TEAMLEAD', true);
}
public function testWithMissingUsername()
public function testWithMissingUsername(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "username").');

View File

@@ -19,10 +19,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
*/
class ReloadCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
protected Application $application;
protected function setUp(): void
{

View File

@@ -21,10 +21,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class TimesheetStopAllCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function setUp(): void
{

View File

@@ -22,10 +22,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class UpdateCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function getCommand(): Command
{
@@ -41,7 +38,7 @@ class UpdateCommandTest extends KernelTestCase
return $this->application->find('kimai:update');
}
public function testFullRun()
public function testFullRun(): void
{
$command = $this->getCommand();
$commandTester = new CommandTester($command);

View File

@@ -21,10 +21,7 @@ use Symfony\Component\Console\Tester\CommandTester;
*/
class VersionCommandTest extends KernelTestCase
{
/**
* @var Application
*/
protected $application;
private Application $application;
protected function setUp(): void
{
@@ -38,14 +35,14 @@ class VersionCommandTest extends KernelTestCase
/**
* @dataProvider getTestData
*/
public function testVersion(array $options, $result)
public function testVersion(array $options, $result): void
{
$commandTester = $this->getCommandTester($options);
$output = $commandTester->getDisplay();
$this->assertEquals($result . PHP_EOL, $output);
}
public function getTestData()
public function getTestData(): array // @phpstan-ignore-line
{
return [
[[], 'Kimai ' . Constants::VERSION . ' by Kevin Papst.'],
@@ -54,7 +51,7 @@ class VersionCommandTest extends KernelTestCase
];
}
protected function getCommandTester(array $options = [])
protected function getCommandTester(array $options = []): CommandTester
{
$command = $this->application->find('kimai:version');
$commandTester = new CommandTester($command);

View File

@@ -10,13 +10,15 @@
namespace App\Tests\DependencyInjection\Compiler;
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
use App\Export\ExportRepositoryInterface;
use App\Export\Renderer\CsvRenderer;
use App\Export\Renderer\HtmlRenderer;
use App\Export\RendererInterface;
use App\Export\ServiceExport;
use App\Export\Timesheet\PDFRenderer;
use App\Export\Timesheet\XlsxRenderer;
use App\Export\TimesheetExportInterface;
use App\Export\TimesheetExportRepository;
use App\Kernel;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@@ -38,17 +40,17 @@ class ExportServiceCompilerPassTest extends TestCase
$renderers = [CsvRenderer::class, HtmlRenderer::class];
foreach ($renderers as $renderer) {
$container->register($renderer)->addTag(Kernel::TAG_EXPORT_RENDERER);
$container->register($renderer)->addTag(RendererInterface::class);
}
$exporters = [PDFRenderer::class, XlsxRenderer::class];
foreach ($exporters as $exporter) {
$container->register($exporter)->addTag(Kernel::TAG_TIMESHEET_EXPORTER);
$container->register($exporter)->addTag(TimesheetExportInterface::class);
}
$repositories = [TimesheetExportRepository::class];
foreach ($repositories as $repository) {
$container->register($repository)->addTag(Kernel::TAG_EXPORT_REPOSITORY);
$container->register($repository)->addTag(ExportRepositoryInterface::class);
}
return $container;

View File

@@ -13,11 +13,14 @@ use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
use App\Invoice\Calculator\DefaultCalculator;
use App\Invoice\Calculator\ShortInvoiceCalculator;
use App\Invoice\Calculator\UserInvoiceCalculator;
use App\Invoice\CalculatorInterface;
use App\Invoice\InvoiceItemRepositoryInterface;
use App\Invoice\NumberGenerator\ConfigurableNumberGenerator;
use App\Invoice\NumberGenerator\DateNumberGenerator;
use App\Invoice\NumberGeneratorInterface;
use App\Invoice\Renderer\DocxRenderer;
use App\Invoice\RendererInterface;
use App\Invoice\ServiceInvoice;
use App\Kernel;
use App\Repository\TimesheetInvoiceItemRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -37,22 +40,22 @@ class InvoiceServiceCompilerPassTest extends TestCase
$renderers = [DocxRenderer::class];
foreach ($renderers as $renderer) {
$container->register($renderer)->addTag(Kernel::TAG_INVOICE_RENDERER);
$container->register($renderer)->addTag(RendererInterface::class);
}
$numberGenerators = [DateNumberGenerator::class, ConfigurableNumberGenerator::class];
foreach ($numberGenerators as $numberGenerator) {
$container->register($numberGenerator)->addTag(Kernel::TAG_INVOICE_NUMBER_GENERATOR);
$container->register($numberGenerator)->addTag(NumberGeneratorInterface::class);
}
$calculators = [DefaultCalculator::class, UserInvoiceCalculator::class, ShortInvoiceCalculator::class];
foreach ($calculators as $calculator) {
$container->register($calculator)->addTag(Kernel::TAG_INVOICE_CALCULATOR);
$container->register($calculator)->addTag(CalculatorInterface::class);
}
$repositories = [TimesheetInvoiceItemRepository::class];
foreach ($repositories as $repository) {
$container->register($repository)->addTag(Kernel::TAG_INVOICE_REPOSITORY);
$container->register($repository)->addTag(InvoiceItemRepositoryInterface::class);
}
return $container;

View File

@@ -10,9 +10,9 @@
namespace App\Tests\DependencyInjection\Compiler;
use App\DependencyInjection\Compiler\WidgetCompilerPass;
use App\Kernel;
use App\Widget\Type\ActiveTimesheets;
use App\Widget\Type\ActiveUsersMonth;
use App\Widget\WidgetInterface;
use App\Widget\WidgetService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -32,7 +32,7 @@ class WidgetCompilerPassTest extends TestCase
$widgets = [ActiveTimesheets::class, ActiveUsersMonth::class];
foreach ($widgets as $widget) {
$container->register($widget)->addTag(Kernel::TAG_WIDGET);
$container->register($widget)->addTag(WidgetInterface::class);
}
return $container;

View File

@@ -12,29 +12,27 @@ namespace App\Tests\Plugin;
use App\Plugin\Plugin;
use App\Plugin\PluginInterface;
use App\Plugin\PluginManager;
use App\Plugin\PluginMetadata;
use App\Tests\Plugin\Fixtures\TestPlugin\TestPlugin;
use App\Tests\Plugin\Fixtures\TestPlugin2\TestPlugin2;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Plugin\PluginManager
* @covers \App\Plugin\Plugin
* @covers \App\Plugin\PluginMetadata
*/
class PluginManagerTest extends TestCase
{
public function testEmptyObject()
public function testEmptyObject(): void
{
$sut = new PluginManager([]);
$this->assertEmpty($sut->getPlugins());
$this->assertNull($sut->getPlugin('foo'));
$this->assertFalse($sut->hasPlugin('foo'));
}
public function testUnknownRendererReturnsNull()
{
$sut = new PluginManager([]);
$this->assertNull($sut->getPlugin('foo'));
}
public function testAdd()
public function testAdd(): void
{
$plugin = $this->createMock(PluginInterface::class);
$plugin->expects($this->any())->method('getName')->willReturn('foo');
@@ -52,27 +50,23 @@ class PluginManagerTest extends TestCase
// make sure a plugin with the same name is not added twice, the first one wins!
$this->assertEquals(2, \count($sut->getPlugins()));
$this->assertFalse($sut->hasPlugin('bar'));
$this->assertTrue($sut->hasPlugin('foo'));
$foo = $sut->getPlugin('foo');
$this->assertInstanceOf(Plugin::class, $foo);
$this->assertEquals('foo', $foo->getName());
$this->assertEquals('foo', $foo->getId());
$this->assertEquals('bar', $foo->getPath());
$test = $sut->getPlugin('TestPlugin');
$this->assertInstanceOf(Plugin::class, $test);
$this->assertEquals('TestPlugin', $test->getName());
$this->assertNull($test->getMetadata());
}
$this->assertEquals('TestPlugin', $test->getId());
$this->assertEquals('TestPlugin from composer.json', $test->getName());
$this->assertInstanceOf(PluginMetadata::class, $test->getMetadata());
public function testLoadMetadata()
{
$sut = new PluginManager([new TestPlugin()]);
$plugin = $sut->getPlugin('TestPlugin');
$sut->loadMetadata($plugin);
$meta = $plugin->getMetadata();
$meta = $test->getMetadata();
$this->assertEquals(10000, $meta->getKimaiVersion());
$this->assertEquals('1.0', $meta->getVersion());
$this->assertEquals('TestPlugin', $plugin->getId());
$this->assertEquals('TestPlugin', $test->getId());
$this->assertEquals('TestPlugin from composer.json', $meta->getName());
$this->assertEquals('Just a test fixture for the PluginManager', $meta->getDescription());
$this->assertEquals('https://github.com/kimai/kimai', $meta->getHomepage());

View File

@@ -17,12 +17,11 @@ use PHPUnit\Framework\TestCase;
*/
class PluginMetadataTest extends TestCase
{
public function testEmptyObject()
public function testNonExistingDirectoryThrowsException(): void
{
$sut = new PluginMetadata();
$this->assertNull($sut->getDescription());
$this->assertNull($sut->getHomepage());
$this->assertNull($sut->getVersion());
$this->assertNull($sut->getKimaiVersion());
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Bundle "Plugin" does not ship composer.json, which is required since 2.0.');
new PluginMetadata(__DIR__);
}
}

View File

@@ -11,7 +11,6 @@ namespace App\Tests\Plugin;
use App\Plugin\Plugin;
use App\Plugin\PluginInterface;
use App\Plugin\PluginMetadata;
use PHPUnit\Framework\TestCase;
/**
@@ -19,22 +18,10 @@ use PHPUnit\Framework\TestCase;
*/
class PluginTest extends TestCase
{
public function testEmptyObject()
public function testEmptyObject(): void
{
$plugin = new Plugin($this->createMock(PluginInterface::class));
$this->assertEquals('', $plugin->getId());
$this->assertEquals('', $plugin->getName());
$this->assertEquals('', $plugin->getPath());
$this->assertNull($plugin->getMetadata());
}
public function testGetterAndSetter()
{
$metadata = new PluginMetadata();
$plugin = new Plugin($this->createMock(PluginInterface::class));
$plugin->setMetadata($metadata);
$this->assertEquals($metadata, $plugin->getMetadata());
}
}

View File

@@ -1067,36 +1067,6 @@ parameters:
count: 1
path: API/Serializer/ValidationFailedExceptionErrorHandlerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testIsSecurePing\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testIsSecurePlugins\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testIsSecureVersion\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testPing\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testPlugins\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Method App\\\\Tests\\\\API\\\\StatusControllerTest\\:\\:testVersion\\(\\) has no return type specified\\.$#"
count: 1
path: API/StatusControllerTest.php
-
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
count: 3
@@ -1307,31 +1277,6 @@ parameters:
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testActivate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testActivateOnActiveUser\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ActivateUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ActivateUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\ActivateUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1392,31 +1337,6 @@ parameters:
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testDeactivate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testDeactivateOnDeactivatedUser\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DeactivateUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DeactivateUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\DeactivateUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1427,46 +1347,6 @@ parameters:
count: 2
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteAdminFailsOnTeamlead\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteRole\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteRoleAndSuperFails\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteSuper\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testDemoteSuperFailsOnTeamlead\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\DemoteUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/DemoteUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\DemoteUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1477,31 +1357,16 @@ parameters:
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandErrors\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandErrors\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandResult\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:assertCommandResult\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:clearExportFiles\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:createApplication\\(\\) has parameter \\$mailer with no type specified\\.$#"
count: 1
@@ -1517,71 +1382,6 @@ parameters:
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:prepareFixtures\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateExportByCustomer\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateExportByProject\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateExportWithEmail\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidDirectory\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidEmail\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidEmails\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidEnd\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithInvalidStart\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithMissingEntries\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithMissingTemplate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithUnknownExportFilter\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:testCreateWithUnknownTemplate\\(\\) has no return type specified\\.$#"
count: 1
path: Command/ExportCreateCommandTest.php
-
message: "#^Parameter \\#1 \\$serviceExport of class App\\\\Command\\\\ExportCreateCommand constructor expects App\\\\Export\\\\ServiceExport, object\\|null given\\.$#"
count: 1
@@ -1617,11 +1417,6 @@ parameters:
count: 1
path: Command/InstallCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\InstallCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/InstallCommandTest.php
-
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 1
@@ -1672,11 +1467,6 @@ parameters:
count: 1
path: Command/InvoiceCreateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PluginCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
@@ -1687,56 +1477,11 @@ parameters:
count: 1
path: Command/PluginCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:testWithPlugins\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PluginCommandTest.php
-
message: "#^Cannot call method getRepository\\(\\) on object\\|null\\.$#"
count: 2
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:callCommand\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testCommandName\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteRole\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteRoleAndSuperFails\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteSuper\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteSuperFailsOnSuperAdmin\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testPromoteTeamleadFailsOnTeamlead\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\PromoteUserCommandTest\\:\\:testWithMissingUsername\\(\\) has no return type specified\\.$#"
count: 1
path: Command/PromoteUserCommandTest.php
-
message: "#^Parameter \\#1 \\$userService of class App\\\\Command\\\\PromoteUserCommand constructor expects App\\\\User\\\\UserService, object\\|null given\\.$#"
count: 1
@@ -1782,31 +1527,11 @@ parameters:
count: 1
path: Command/UpdateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\UpdateCommandTest\\:\\:testFullRun\\(\\) has no return type specified\\.$#"
count: 1
path: Command/UpdateCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:getCommandTester\\(\\) has no return type specified\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:getCommandTester\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:getTestData\\(\\) has no return type specified\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:testVersion\\(\\) has no return type specified\\.$#"
count: 1
path: Command/VersionCommandTest.php
-
message: "#^Method App\\\\Tests\\\\Command\\\\VersionCommandTest\\:\\:testVersion\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
@@ -6937,81 +6662,6 @@ parameters:
count: 1
path: Pdf/PdfContextTest.php
-
message: "#^Cannot call method getDescription\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getHomepage\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getId\\(\\) on App\\\\Plugin\\\\Plugin\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getKimaiVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getMetadata\\(\\) on App\\\\Plugin\\\\Plugin\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getName\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Cannot call method getVersion\\(\\) on App\\\\Plugin\\\\PluginMetadata\\|null\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testAdd\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testEmptyObject\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testLoadMetadata\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginManagerTest\\:\\:testUnknownRendererReturnsNull\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Parameter \\#1 \\$plugin of method App\\\\Plugin\\\\PluginManager\\:\\:loadMetadata\\(\\) expects App\\\\Plugin\\\\Plugin, App\\\\Plugin\\\\Plugin\\|null given\\.$#"
count: 1
path: Plugin/PluginManagerTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginMetadataTest\\:\\:testEmptyObject\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginMetadataTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginTest\\:\\:testEmptyObject\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginTest.php
-
message: "#^Method App\\\\Tests\\\\Plugin\\\\PluginTest\\:\\:testGetterAndSetter\\(\\) has no return type specified\\.$#"
count: 1
path: Plugin/PluginTest.php
-
message: "#^Method App\\\\Tests\\\\Project\\\\ProjectServiceTest\\:\\:testCannotSavePersistedProjectAsNew\\(\\) has no return type specified\\.$#"
count: 1