release 1.17.2 (#3135)

* allow to input password interactively in console
* added block to simplify overwriting export template parts
* prevent installation in PHP 8.1
* composer update
* phpunit version to 9
* support negative amounts in excel export
* added system configuration actions for calendar, weekly timesheet, users
* added method to render text with full markdown support
This commit is contained in:
Kevin Papst
2022-02-14 17:33:52 +01:00
committed by GitHub
parent 4cb0ac37a6
commit 83a894823f
37 changed files with 1298 additions and 892 deletions

View File

@@ -19,7 +19,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
matrix:
php: ['7.4']
php: ['8.0']
name: Coverage (${{ matrix.php }})
steps:

View File

@@ -19,7 +19,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
matrix:
php: ['7.3', '7.4', '8.0']
php: ['7.4', '8.0', '8.1']
name: Integration (${{ matrix.php }})
steps:

View File

@@ -10,7 +10,7 @@
}
],
"require": {
"php": ">=7.3",
"php": "7.3.*||7.4.*||8.0.*",
"ext-gd": "*",
"ext-intl": "*",
"ext-json": "*",
@@ -78,12 +78,12 @@
"dama/doctrine-test-bundle": "^6.0",
"doctrine/doctrine-fixtures-bundle": "^3.2",
"fakerphp/faker": "^1.15",
"friendsofphp/php-cs-fixer": "3.2.*",
"friendsofphp/php-cs-fixer": "^3.2",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-symfony": "^1.0",
"phpunit/phpunit": "^8.0",
"phpunit/phpunit": "^9.0",
"symfony/browser-kit": "^4.4",
"symfony/css-selector": "^4.4",
"symfony/debug-bundle": "^4.4",

1765
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php">
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php">
<php>
<ini name="error_reporting" value="-1" />
<ini name="max_execution_time" value="-1" />
<ini name="intl.default_locale" value="en_US" />
<ini name="error_reporting" value="-1"/>
<ini name="max_execution_time" value="-1"/>
<ini name="intl.default_locale" value="en_US"/>
<env name="KERNEL_CLASS" value="App\Kernel" force="true"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
<env name="APP_ENV" value="test" force="true"/>
<env name="APP_DEBUG" value="1" force="true"/>
<env name="APP_SECRET" value="5a79a1c866efef9ca1800f971d689f3e" force="true"/>
<!--
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
CREATE DATABASE IF NOT EXISTS `kimai2_test`;
CREATE USER IF NOT EXISTS `kimai2_test`@127.0.0.1;
ALTER USER `kimai2_test`@127.0.0.1 IDENTIFIED BY "kimai2_test";
GRANT execute,select,insert,update,delete,create,alter,drop,index,references ON `kimai2_test`.* TO kimai2_test@127.0.0.1;
-->
<env name="DATABASE_URL" value="mysql://kimai2_test:kimai2_test@127.0.0.1:3306/kimai2_test"/>
<env name="CORS_ALLOW_ORIGIN" value="^https?://localhost(:[0-9]+)?$"/>
<env name="MAILER_URL" value="null://null"/>
<env name="MAILER_FROM" value="kimai@example.com"/>
<!--
REINSTALL THE TEST DATABASE, eg. AFTER CHANGING STRUCTURE!
@@ -41,34 +37,34 @@
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src/</directory>
<directory suffix=".php">templates/</directory>
<exclude>
<directory suffix=".php">src/Migrations/</directory>
<directory suffix=".php">assets/</directory>
<directory suffix=".php">bin/</directory>
<directory suffix=".php">config/</directory>
<directory suffix=".php">node_modules/</directory>
<directory suffix=".php">public/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">translations/</directory>
<directory suffix=".php">var/</directory>
<directory suffix=".php">vendor/</directory>
</exclude>
</whitelist>
</filter>
</include>
<exclude>
<directory suffix=".php">src/Migrations/</directory>
<directory suffix=".php">assets/</directory>
<directory suffix=".php">bin/</directory>
<directory suffix=".php">config/</directory>
<directory suffix=".php">node_modules/</directory>
<directory suffix=".php">public/</directory>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">translations/</directory>
<directory suffix=".php">var/</directory>
<directory suffix=".php">vendor/</directory>
</exclude>
</coverage>
<!--
wrap tests inside database transactions, so tests can safely manipulate contents without
affecting other tests. @see https://github.com/dmaicher/doctrine-test-bundle
-->
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" />
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>
</phpunit>
</phpunit>

View File

@@ -0,0 +1,40 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
abstract class AbstractUserCommand extends Command
{
protected function askForPassword(InputInterface $input, OutputInterface $output): string
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$passwordQuestion = new Question('Please enter the password: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$passwordQuestion->setValidator(function (?string $value) {
$password = trim($value);
if (empty($password)) {
throw new \Exception('The password may not be empty');
}
return $value;
});
$passwordQuestion->setMaxAttempts(3);
return $helper->ask($input, $output, $passwordQuestion);
}
}

View File

@@ -12,12 +12,11 @@ namespace App\Command;
use App\User\UserService;
use App\Utils\CommandStyle;
use App\Validator\ValidationFailedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ChangePasswordCommand extends Command
final class ChangePasswordCommand extends AbstractUserCommand
{
private $userService;
@@ -35,7 +34,7 @@ class ChangePasswordCommand extends Command
->setDescription('Change the password of a user.')
->setDefinition([
new InputArgument('username', InputArgument::REQUIRED, 'The username'),
new InputArgument('password', InputArgument::REQUIRED, 'The password'),
new InputArgument('password', InputArgument::OPTIONAL, 'The password'),
])
->setHelp(
<<<'EOT'
@@ -59,7 +58,12 @@ EOT
protected function execute(InputInterface $input, OutputInterface $output)
{
$username = $input->getArgument('username');
$password = $input->getArgument('password');
if (null !== $input->getArgument('password')) {
$password = $input->getArgument('password');
} else {
$password = $this->askForPassword($input, $output);
}
$user = $this->userService->findUserByUsernameOrThrowException($username);

View File

@@ -13,14 +13,11 @@ use App\Entity\User;
use App\User\UserService;
use App\Utils\CommandStyle;
use App\Validator\ValidationFailedException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
final class CreateUserCommand extends Command
final class CreateUserCommand extends AbstractUserCommand
{
private $userService;
@@ -91,31 +88,4 @@ final class CreateUserCommand extends Command
return 0;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return string
*/
protected function askForPassword(InputInterface $input, OutputInterface $output): string
{
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$passwordQuestion = new Question('Please enter the password: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$passwordQuestion->setValidator(function (?string $value) {
$password = trim($value);
if (empty($password)) {
throw new \Exception('The password may not be empty');
}
return $value;
});
$passwordQuestion->setMaxAttempts(3);
return $helper->ask($input, $output, $passwordQuestion);
}
}

View File

@@ -21,10 +21,7 @@ final class ThemeConfiguration implements \ArrayAccess
$this->systemConfiguration = $systemConfiguration;
}
/**
* @return bool
*/
public function offsetExists($offset)
public function offsetExists($offset): bool
{
return $this->systemConfiguration->has('theme.' . $offset);
}
@@ -42,7 +39,7 @@ final class ThemeConfiguration implements \ArrayAccess
* @param mixed $value
* @throws \BadMethodCallException
*/
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): void
{
throw new \BadMethodCallException('ThemeConfiguration does not support offsetSet()');
}
@@ -51,7 +48,7 @@ final class ThemeConfiguration implements \ArrayAccess
* @param mixed $offset
* @throws \BadMethodCallException
*/
public function offsetUnset($offset)
public function offsetUnset($offset): void
{
throw new \BadMethodCallException('ThemeConfiguration does not support offsetUnset()');
}

View File

@@ -24,6 +24,10 @@ class CalendarSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('timesheet_create'));
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'calendar']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('calendar.html'));
}
}

View File

@@ -20,6 +20,10 @@ class QuickEntrySubscriber extends AbstractActionsSubscriber
public function onActions(PageActionsEvent $event): void
{
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'quick_entry']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('weekly-times.html'));
}
}

View File

@@ -38,6 +38,10 @@ class UsersSubscriber extends AbstractActionsSubscriber
$event->addCreate($this->path('admin_user_create'), false);
}
if ($this->isGranted('system_configuration')) {
$event->addAction('settings', ['url' => $this->path('system_configuration_section', ['section' => 'user']), 'class' => 'modal-ajax-form']);
}
$event->addHelp($this->documentationLink('users.html'));
}
}

View File

@@ -45,8 +45,10 @@ abstract class AbstractSpreadsheetRenderer
public const TIME_FORMAT = 'hh:mm';
public const DURATION_FORMAT = '[hh]:mm';
public const DURATION_DECIMAL = '#0.00';
// https://support.microsoft.com/de-de/office/zahlenformatcodes-5026bbd6-04bc-48cd-bf33-80f18b4eae68
// Part 1 = positive; Part 2 = negative; Part 3 = zero; Part 4 = Text
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* \(#,##0.00\);_("%1$s"* "-"??_);_(@_)';
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* -#,##0.00;_("%1$s"* "-"??_);_(@_)';
public const RATE_FORMAT = self::RATE_FORMAT_LEFT;
protected $durationFormat = self::DURATION_FORMAT;

View File

@@ -49,11 +49,11 @@ class PDFRenderer
*/
private $pdfOptions = [];
public function __construct(Environment $twig, HtmlToPdfConverter $converter, ProjectStatisticService $projectRepository)
public function __construct(Environment $twig, HtmlToPdfConverter $converter, ProjectStatisticService $projectStatisticService)
{
$this->twig = $twig;
$this->converter = $converter;
$this->projectStatisticService = $projectRepository;
$this->projectStatisticService = $projectStatisticService;
}
protected function getTemplate(): string

View File

@@ -53,7 +53,6 @@ class TimesheetEditForm extends AbstractType
$project = null;
$customer = null;
$currency = false;
$begin = null;
$customerCount = $this->customers->countCustomer(true);
$timezone = $options['timezone'];
$isNew = true;

View File

@@ -126,6 +126,6 @@ final class MarkdownExtension implements RuntimeExtensionInterface
*/
public function markdownToHtml(string $content): string
{
return $this->markdown->toHtml($content);
return $this->markdown->withFullMarkdownSupport($content);
}
}

View File

@@ -33,14 +33,14 @@ class Duration
* @param string $format
* @return string|null
*/
public function format($seconds, $format = self::FORMAT_NO_SECONDS)
public function format(?int $seconds, string $format = self::FORMAT_NO_SECONDS)
{
if (null === $seconds) {
return null;
}
$hour = floor($seconds / 3600);
$minute = floor(($seconds / 60) % 60);
$hour = (int) floor($seconds / 3600);
$minute = (int) floor((int) ($seconds / 60) % 60);
$hour = $hour > 9 ? $hour : '0' . $hour;
$minute = $minute > 9 ? $minute : '0' . $minute;
@@ -60,7 +60,7 @@ class Duration
* @param string $duration
* @return int
*/
public function parseDurationString($duration): int
public function parseDurationString(string $duration): int
{
if (false !== stripos($duration, ':')) {
return $this->parseDuration($duration, self::FORMAT_COLON);

View File

@@ -83,7 +83,6 @@ class MPdfConverter implements HtmlToPdfConverter
for ($i = 0; $i < \count($parts); $i++) {
if (stripos($parts[$i], '<!-- CONTENT_PART -->') !== false) {
$subParts = explode('<!-- CONTENT_PART -->', $parts[$i]);
$run = 0;
foreach ($subParts as $subPart) {
$mpdf->WriteHTML($subPart);
}

View File

@@ -10,7 +10,7 @@
namespace App\Utils;
/**
* A simple class to parse markdown syntax and return HTML.
* Parse markdown syntax and return HTML.
*/
final class Markdown
{
@@ -19,20 +19,14 @@ final class Markdown
*/
private $parser;
public function __construct()
{
$this->parser = new ParsedownExtension();
$this->parser->setUrlsLinked(true);
$this->parser->setBreaksEnabled(true);
}
/**
* @param string $text
* @param bool $safe
* @return string
*/
public function toHtml(string $text, bool $safe = true): string
{
if ($this->parser === null) {
$this->parser = new ParsedownExtension();
$this->parser->setUrlsLinked(true);
$this->parser->setBreaksEnabled(true);
}
if ($safe !== true) {
@trigger_error('Only safe mode is supported in Markdown since 1.16.3 to prevent XSS attacks. Parameter $safe will be removed with 2.0', E_USER_DEPRECATED);
}
@@ -42,4 +36,15 @@ final class Markdown
return $this->parser->text($text);
}
public function withFullMarkdownSupport(string $text): string
{
$parser = new \Parsedown();
$parser->setUrlsLinked(true);
$parser->setBreaksEnabled(true);
$parser->setSafeMode(true);
$parser->setMarkupEscaped(true);
return $parser->text($text);
}
}

View File

@@ -24,7 +24,7 @@ class ParsedownExtension extends \Parsedown
protected $BlockTypes = [
'*' => ['Rule', 'List'],
'+' => ['List'],
'-' => ['SetextHeader', 'Table', 'Rule', 'List'],
'-' => ['Table', 'Rule', 'List'],
'0' => ['List'],
'1' => ['List'],
'2' => ['List'],
@@ -37,7 +37,6 @@ class ParsedownExtension extends \Parsedown
'9' => ['List'],
':' => ['Table'],
'<' => ['Comment', 'Markup'],
'=' => ['SetextHeader'],
'>' => ['Quote'],
'[' => ['Reference'],
'_' => ['Rule'],
@@ -124,7 +123,7 @@ class ParsedownExtension extends \Parsedown
* @param string $text
* @return string
*/
private function getIDfromText($text)
private function getIDfromText($text): string
{
$text = strtolower($text);

View File

@@ -291,15 +291,15 @@
"phpunit/php-file-iterator": {
"version": "1.4.5"
},
"phpunit/php-invoker": {
"version": "3.1.1"
},
"phpunit/php-text-template": {
"version": "1.2.1"
},
"phpunit/php-timer": {
"version": "1.0.9"
},
"phpunit/php-token-stream": {
"version": "2.0.2"
},
"phpunit/phpunit": {
"version": "4.7",
"recipe": {
@@ -333,12 +333,21 @@
"robrichards/xmlseclibs": {
"version": "3.0.4"
},
"sebastian/cli-parser": {
"version": "1.0.1"
},
"sebastian/code-unit": {
"version": "1.0.8"
},
"sebastian/code-unit-reverse-lookup": {
"version": "1.0.1"
},
"sebastian/comparator": {
"version": "2.1.1"
},
"sebastian/complexity": {
"version": "2.0.2"
},
"sebastian/diff": {
"version": "2.0.1"
},
@@ -351,6 +360,9 @@
"sebastian/global-state": {
"version": "2.0.0"
},
"sebastian/lines-of-code": {
"version": "1.0.3"
},
"sebastian/object-enumerator": {
"version": "3.0.3"
},

View File

@@ -35,7 +35,7 @@
</div>
{{ widgets.user_avatar(comment.createdBy, false, 'direct-chat-img img-sm') }}
<div class="direct-chat-text">
{{ comment.message|replace(replacer)|md2html }}
{{ comment.message|replace(replacer)|comment2html }}
</div>
</div>
{% endfor %}

View File

@@ -103,15 +103,18 @@
<sethtmlpagefooter name="myfooter" value="on" />
mpdf-->
{% endblock %}
{% block body_start %}{% endblock %}
{% block summary %}
<h2 style="margin-bottom: 4px; padding-bottom: 0">{% block title %}{{ 'export.document_title'|trans }}{% endblock %}</h2>
{% if query is defined %}
<p>
{{ 'export.period'|trans }}:
{{ query.begin|date_short }} - {{ query.end|date_short }}
</p>
{% endif %}
<h3>{{ 'export.summary'|trans }}</h3>
{% block summary_header %}
<h2 style="margin-bottom: 4px; padding-bottom: 0">{% block title %}{{ 'export.document_title'|trans }}{% endblock %}</h2>
{% if query is defined %}
<p>
{{ 'export.period'|trans }}:
{{ query.begin|date_short }} - {{ query.end|date_short }}
</p>
{% endif %}
<h3>{{ 'export.summary'|trans }}</h3>
{% endblock %}
<table class="items">
<thead>
<tr>
@@ -254,7 +257,9 @@ mpdf-->
<pagebreak>
{% endblock %}
{% block items %}
<h3>{{ 'export.full_list'|trans }}</h3>
{% block items_header %}
<h3>{{ 'export.full_list'|trans }}</h3>
{% endblock %}
{% set duration = 0 %}
{% set rate = 0 %}
@@ -338,5 +343,6 @@ mpdf-->
</tbody>
</table>
{% endblock %}
{% block body_end %}{% endblock %}
</body>
</html>

View File

@@ -6,9 +6,9 @@
{% import "invoice/macros.html.twig" as macros %}
{% set columns = {
'date': {'class': 'alwaysVisible'},
'date': {'class': 'alwaysVisible text-nowrap'},
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
'customer': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
} %}
{% for field in metaColumns %}

View File

@@ -20,6 +20,7 @@ use Symfony\Component\Console\Tester\CommandTester;
/**
* @covers \App\Command\ChangePasswordCommand
* @covers \App\Command\AbstractUserCommand
* @group integration
*/
class ChangePasswordCommandTest extends KernelTestCase
@@ -59,6 +60,7 @@ class ChangePasswordCommandTest extends KernelTestCase
$input = [
'command' => $command->getName(),
];
$interactive = false;
if ($username !== null) {
$input['username'] = $username;
@@ -66,10 +68,19 @@ class ChangePasswordCommandTest extends KernelTestCase
if ($password !== null) {
$input['password'] = $password;
} else {
$interactive = true;
}
$commandTester = new CommandTester($command);
$commandTester->execute($input);
$options = [];
if ($interactive) {
$options = ['interactive' => true];
$commandTester->setInputs(['12345678']);
}
$commandTester->execute($input, $options);
return $commandTester;
}
@@ -100,11 +111,10 @@ class ChangePasswordCommandTest extends KernelTestCase
$this->callCommand(null, '1234567890');
}
public function testWithMissingPassword()
public function testWithMissingPasswordAsksForPassword()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "password").');
$this->callCommand('1234567890', null);
$commandTester = $this->callCommand('john_user', null);
$output = $commandTester->getDisplay();
$this->assertStringContainsString('[OK] Changed password for user "john_user".', $output);
}
}

View File

@@ -16,7 +16,6 @@ use App\Tests\Configuration\TestConfigLoader;
use App\Tests\Mocks\Saml\SamlAuthFactoryFactory;
use OneLogin\Saml2\Auth;
use PHPUnit\Framework\TestCase;
use PHPUnit\Util\Xml;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
@@ -83,7 +82,7 @@ class SamlControllerTest extends TestCase
public function testMetadataAction()
{
$expected = <<<EOD
$expectedXmlString = <<<EOD
<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2020-07-23T10:26:50Z" cacheDuration="PT604800S" entityID="https://127.0.0.1:8010/auth/saml/metadata">
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
@@ -118,8 +117,13 @@ EOD;
self::assertInstanceOf(Response::class, $result);
self::assertEquals('xml', $result->headers->get('Content-Type'));
$expected = Xml::load($expected);
$actual = Xml::load($result->getContent());
$expected = new \DOMDocument();
$tmp = $expected->loadXML($expectedXmlString);
self::assertTrue($tmp);
$actual = new \DOMDocument();
$tmp = $actual->loadXML($result->getContent());
self::assertTrue($tmp);
// the "validUntil" attribute in the outer node changes per request
self::assertEquals($expected->firstChild->firstChild, $actual->firstChild->firstChild);

View File

@@ -10,6 +10,7 @@
namespace App\Tests\Controller;
use App\Configuration\SystemConfiguration;
use App\Entity\User;
use App\Tests\Configuration\TestConfigLoader;
use App\Tests\DataFixtures\TimesheetFixtures;
@@ -33,6 +34,11 @@ class CalendarControllerTest extends ControllerBaseTest
$this->request($client, '/calendar/');
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertPageActions($client, [
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
'help' => 'https://www.kimai.org/documentation/calendar.html'
]);
$crawler = $client->getCrawler();
$calendar = $crawler->filter('div#timesheet_calendar');
$this->assertEquals(1, $calendar->count());
@@ -40,6 +46,18 @@ class CalendarControllerTest extends ControllerBaseTest
$this->assertEquals(1, $dragAndDropBoxes->count());
}
public function testCalendarActionAsSuperAdmin()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/calendar/');
$this->assertPageActions($client, [
'create modal-ajax-form' => $this->createUrl('/timesheet/create'),
'settings modal-ajax-form' => $this->createUrl('/admin/system-config/edit/calendar'),
'help' => 'https://www.kimai.org/documentation/calendar.html'
]);
}
public function testCalendarActionWithGoogleSource()
{
$loader = new TestConfigLoader([]);

View File

@@ -9,6 +9,7 @@
namespace App\Tests\Controller;
use App\Configuration\SystemConfiguration;
use App\Entity\Customer;
use App\Entity\CustomerComment;
use App\Entity\CustomerMeta;
@@ -161,6 +162,13 @@ class CustomerControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/1/details'));
$client->followRedirect();
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
self::assertStringContainsString('A beautiful and short comment **with some** markdown formatting', $node->html());
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
$configService->offsetSet('timesheet.markdown_content', true);
$this->assertAccessIsGranted($client, '/admin/customer/1/details');
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
self::assertStringContainsString('<p>A beautiful and short comment <strong>with some</strong> markdown formatting</p>', $node->html());
}

View File

@@ -9,6 +9,7 @@
namespace App\Tests\Controller;
use App\Configuration\SystemConfiguration;
use App\Entity\Activity;
use App\Entity\ActivityMeta;
use App\Entity\ActivityRate;
@@ -260,6 +261,13 @@ class ProjectControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/admin/project/1/details'));
$client->followRedirect();
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
self::assertStringContainsString('A beautiful and long comment **with some** markdown formatting', $node->html());
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
$configService = static::$kernel->getContainer()->get(SystemConfiguration::class);
$configService->offsetSet('timesheet.markdown_content', true);
$this->assertAccessIsGranted($client, '/admin/project/1/details');
$node = $client->getCrawler()->filter('div.box#comments_box .direct-chat-text');
self::assertStringContainsString('<p>A beautiful and long comment <strong>with some</strong> markdown formatting</p>', $node->html());
}

View File

@@ -39,6 +39,7 @@ class UserControllerTest extends ControllerBaseTest
'visibility' => '#',
'download toolbar-action' => $this->createUrl('/admin/user/export'),
'create' => $this->createUrl('/admin/user/create'),
'settings modal-ajax-form' => $this->createUrl('/admin/system-config/edit/user'),
'help' => 'https://www.kimai.org/documentation/users.html'
]);
}

View File

@@ -10,7 +10,6 @@
namespace App\Tests\DependencyInjection;
use App\DependencyInjection\AppExtension;
use PHPUnit\Framework\Error\Notice;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -301,7 +300,7 @@ class AppExtensionTest extends TestCase
*/
public function testDurationOnlyDeprecationIsTriggered()
{
$this->expectException(Notice::class);
$this->expectNotice();
$this->expectExceptionMessage('Found ambiguous configuration: remove "kimai.timesheet.duration_only" and set "kimai.timesheet.mode" instead.');
$minConfig = $this->getMinConfig();
@@ -449,7 +448,7 @@ class AppExtensionTest extends TestCase
public function testWithBundleConfigurationFailsOnDuplicatedKey()
{
$this->expectException(Notice::class);
$this->expectNotice();
$this->expectExceptionMessage('Invalid bundle configuration "timesheet" found, skipping');
$bundleConfig = [
@@ -463,7 +462,7 @@ class AppExtensionTest extends TestCase
public function testWithBundleConfigurationFailsOnNonArray()
{
$this->expectException(Notice::class);
$this->expectNotice();
$this->expectExceptionMessage('Invalid bundle configuration found, skipping all bundle configuration');
$container = $this->getContainer();

View File

@@ -163,7 +163,18 @@ abstract class AbstractRendererTest extends KernelTestCase
->setMetaField((new TimesheetMeta())->setName('foo2')->setValue('meta-bar2')->setIsVisible(true))
;
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
$timesheet6 = new Timesheet();
$timesheet6
->setDuration(400)
->setFixedRate(-100.92)
->setUser((new User())->setUsername('nivek'))
->setActivity($activity)
->setProject($project)
->setBegin(new \DateTime('2019-06-16 12:00:00'))
->setEnd(new \DateTime('2019-06-16 12:06:40'))
;
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5, $timesheet6];
$query = new TimesheetQuery();
$query->setActivities([$activity]);

View File

@@ -34,7 +34,7 @@ class CsvRendererTest extends AbstractRendererTest
public function getTestModel()
{
return [
['400', '2437.12', ' EUR 1,947.99 ', 6, 5, 1, 2, 2]
['400', '2437.12', ' EUR 1,947.99 ', 7, 6, 1, 2, 2]
];
}
@@ -107,8 +107,40 @@ class CsvRendererTest extends AbstractRendererTest
27 => 'ORDER-123',
];
self::assertEquals(6, \count($all));
$expected2 = [
0 => '2019-06-16',
1 => '12:00',
2 => '12:06',
3 => '400',
4 => '0',
5 => '',
6 => 'nivek',
7 => 'nivek',
8 => 'Customer Name',
9 => 'project name',
10 => 'activity description',
11 => '',
12 => '',
13 => '',
14 => '',
15 => '',
16 => ' EUR -100.92',
17 => '',
18 => '',
19 => 'customer-bar',
20 => '',
21 => 'project-foo2',
22 => 'activity-bar',
23 => 'timesheet',
24 => 'work',
25 => 'A-0123456789',
26 => 'DE-9876543210',
27 => 'ORDER-123',
];
self::assertEquals(7, \count($all));
self::assertEquals($expected, $all[5]);
self::assertEquals($expected2, $all[6]);
self::assertEquals(\count($expected), \count($all[0]));
self::assertEquals('foo', $all[4][14]);
}

View File

@@ -82,11 +82,12 @@ class HtmlRendererTest extends AbstractRendererTest
$this->assertStringContainsString('<td>Customer Name</td>', $content);
$this->assertStringContainsString('<td>project name</td>', $content);
$this->assertStringContainsString('<span class="duration-format" data-duration="6600">01:50 h</span>', $content);
$this->assertStringContainsString('<span class="duration-format" data-duration="7000">01:56 h</span>', $content);
$this->assertStringContainsString('<td class="cost summary-rate">€2,437.12</td>', $content);
$this->assertStringContainsString('-€100.92', $content);
// 5 times in the "full list" and once in the "summary with activities"
$this->assertEquals(6, substr_count($content, 'activity description'));
$this->assertEquals(7, substr_count($content, 'activity description'));
$this->assertEquals(1, substr_count($content, '<td>activity description</td>'));
}
}

View File

@@ -26,7 +26,7 @@ class MarkdownExtensionTest extends TestCase
$config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config);
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
$this->assertEquals('<p># foobar</p>', $sut->markdownToHtml('# foobar'));
$this->assertEquals('<h1>foobar</h1>', $sut->markdownToHtml('# foobar'));
$this->assertEquals(
'<p><a href="javascript%3Aalert(`XSS`)">XSS</a></p>',
$sut->markdownToHtml('[XSS](javascript:alert(`XSS`))')
@@ -44,6 +44,9 @@ class MarkdownExtensionTest extends TestCase
);
$this->assertEquals('', $sut->timesheetContent(null));
$this->assertEquals('', $sut->timesheetContent(''));
$this->assertEquals('# foobar', $sut->timesheetContent('# foobar'));
$this->assertEquals('## foobar', $sut->timesheetContent('## foobar'));
$this->assertEquals('### foobar', $sut->timesheetContent('### foobar'));
$config = new SystemConfiguration($loader, ['timesheet' => ['markdown_content' => true]]);
$sut = new MarkdownExtension(new Markdown(), $config);
@@ -75,6 +78,9 @@ class MarkdownExtensionTest extends TestCase
$this->assertEquals('', $sut->commentContent(null));
$this->assertEquals('', $sut->commentContent(''));
$this->assertEquals('# foobar', $sut->commentContent('# foobar'));
$this->assertEquals('## foobar', $sut->commentContent('## foobar'));
$this->assertEquals('### foobar', $sut->commentContent('### foobar'));
$this->assertEquals('<p>' . $loremIpsum . '</p>', $sut->commentContent($loremIpsum, true));
$this->assertEquals('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l &hellip;', $sut->commentContent($loremIpsum));

View File

@@ -13,7 +13,11 @@ use Symfony\Component\Dotenv\Dotenv;
require __DIR__ . '/../vendor/autoload.php';
(new Dotenv(false))->loadEnv(dirname(__DIR__) . '/.env');
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$env = $_SERVER['APP_ENV'] ?? 'prod';
$debug = (bool) ($_SERVER['APP_DEBUG'] ?? (in_array($env, ['dev', 'test'])));
$kernel = new Kernel($env, $debug);
$kernel->boot();
return $kernel->getContainer()->get('doctrine')->getManager();

View File

@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="he" datatype="plaintext" original="export.en.xlf">
<body>
<trans-unit id="oRRnhwf" resname="default.pdf.twig">
<trans-unit id="IbERy.5" resname="default.pdf.twig">
<source>Default</source>
<target>ברירת מחדל</target>
</trans-unit>