3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,9 @@
|
||||
rector.php
|
||||
phpstan.sh
|
||||
|
||||
# custom apache rules e.g. to deactivate ioncube loader
|
||||
public/.user.ini
|
||||
|
||||
# for symfony local webserver
|
||||
php.ini
|
||||
.php-version
|
||||
|
||||
@@ -52,6 +52,15 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
let plugins = ['change_listener'];
|
||||
|
||||
const isMultiple = node.multiple !== undefined && node.multiple === true;
|
||||
const isRequired = node.required !== undefined && node.required === true;
|
||||
|
||||
if (isRequired) {
|
||||
plugins.push('no_backspace_delete');
|
||||
}
|
||||
|
||||
if (isMultiple) {
|
||||
plugins.push('remove_button');
|
||||
}
|
||||
|
||||
/*
|
||||
const isOrdering = false;
|
||||
@@ -61,13 +70,9 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
||||
}
|
||||
*/
|
||||
|
||||
if (isMultiple) {
|
||||
plugins.push('remove_button');
|
||||
}
|
||||
|
||||
let options = {
|
||||
lockOptgroupOrder: true,
|
||||
allowEmptyOption: true,
|
||||
allowEmptyOption: !isRequired,
|
||||
plugins: plugins,
|
||||
// if there are more than X entries, the other ones are hidden and can only be found
|
||||
// by typing some characters to trigger the internal option search
|
||||
|
||||
@@ -1685,11 +1685,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/DependencyInjection/AppExtension.php
|
||||
|
||||
-
|
||||
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
|
||||
count: 2
|
||||
path: src/DependencyInjection/Compiler/ExportServiceCompilerPass.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\DependencyInjection\\\\Configuration\\:\\:getProjectNode\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
|
||||
2
public/build/app.902831bc.js
Normal file
2
public/build/app.902831bc.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.f0079159.js",
|
||||
"/build/app.cd312ca8.js"
|
||||
"/build/app.902831bc.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.b10b16e5.css"
|
||||
@@ -63,7 +63,7 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||
"/build/app.cd312ca8.js": "sha384-lS1VK+quldXkQU0DjsipiX8sXWvojX/NY0+kAUHFh69HlZZG36ubhkNbcV0crl35",
|
||||
"/build/app.902831bc.js": "sha384-t6GwvEu6PBNMxOHW4JHSScxFmGE7tGus/IZoBBZHrGFdtDZtKjweNAqJ00VmSnSp",
|
||||
"/build/app.b10b16e5.css": "sha384-dwEgmHAQRm4PP7XTYkJfIoB+Iumd+0WuH5FyoC/+qC3WQ/e1gCTmlx9M2mIBxPuK",
|
||||
"/build/export-pdf.587575e7.js": "sha384-J50GStmmfVwUTN4dIRQ02eg9hyzGFPSzpTtpPody92j0V6zCqw+s5l8+ZhVTugeW",
|
||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.css": "/build/app.b10b16e5.css",
|
||||
"build/app.js": "/build/app.cd312ca8.js",
|
||||
"build/app.js": "/build/app.902831bc.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
"build/export-pdf.js": "/build/export-pdf.587575e7.js",
|
||||
"build/invoice.css": "/build/invoice.d0ae14e1.css",
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.15';
|
||||
public const VERSION = '2.0.16';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20015;
|
||||
public const VERSION_ID = 20016;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -406,7 +406,7 @@ final class ProfileController extends AbstractController
|
||||
return $this->redirectToRoute('user_profile_2fa', ['username' => $profile->getUserIdentifier()]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{username}/totp.png', name: 'user_profile_2fa_image', methods: ['GET'])]
|
||||
#[Route(path: '/{username}/totp-qr-code', name: 'user_profile_2fa_image', methods: ['GET'])]
|
||||
#[IsGranted('2fa', 'profile')]
|
||||
public function displayTotpQrCode(User $profile, TotpAuthenticatorInterface $totpAuthenticator): Response
|
||||
{
|
||||
|
||||
@@ -9,15 +9,10 @@
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Export\Renderer\HtmlRenderer;
|
||||
use App\Export\Renderer\HtmlRendererFactory;
|
||||
use App\Export\Renderer\PDFRenderer;
|
||||
use App\Export\Renderer\PdfRendererFactory;
|
||||
use App\Export\ServiceExport;
|
||||
use App\Kernel;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
@@ -44,44 +39,15 @@ final class ExportServiceCompilerPass implements CompilerPassInterface
|
||||
$definition->addMethodCall('addExportRepository', [new Reference($id)]);
|
||||
}
|
||||
|
||||
$path = \dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||
foreach ($container->getParameter('kimai.export.documents') as $exportPath) {
|
||||
if (!is_dir($path . $exportPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (glob($path . $exportPath . '/*.html.twig') as $htmlTpl) {
|
||||
$tplName = basename($htmlTpl);
|
||||
if (stripos($tplName, '-bundle') !== false) {
|
||||
$exportDocuments = $container->getParameter('kimai.export.documents');
|
||||
if (\is_array($exportDocuments)) {
|
||||
$path = \dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||
foreach ($exportDocuments as $exportPath) {
|
||||
if (!is_dir($path . $exportPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
|
||||
|
||||
$factoryDefinition = new Definition(HtmlRenderer::class);
|
||||
$factoryDefinition->addArgument($tplName);
|
||||
$factoryDefinition->addArgument($tplName);
|
||||
$factoryDefinition->setFactory([new Reference(HtmlRendererFactory::class), 'create']);
|
||||
|
||||
$container->setDefinition($serviceId, $factoryDefinition);
|
||||
$definition->addMethodCall('addRenderer', [new Reference($serviceId)]);
|
||||
}
|
||||
|
||||
foreach (glob($path . $exportPath . '/*.pdf.twig') as $pdfHtml) {
|
||||
$tplName = basename($pdfHtml);
|
||||
if (stripos($tplName, '-bundle') !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$serviceId = 'exporter_renderer.' . str_replace('.', '_', $tplName);
|
||||
|
||||
$factoryDefinition = new Definition(PDFRenderer::class);
|
||||
$factoryDefinition->addArgument($tplName);
|
||||
$factoryDefinition->addArgument($tplName);
|
||||
$factoryDefinition->setFactory([new Reference(PdfRendererFactory::class), 'create']);
|
||||
|
||||
$container->setDefinition($serviceId, $factoryDefinition);
|
||||
$definition->addMethodCall('addRenderer', [new Reference($serviceId)]);
|
||||
$definition->addMethodCall('addDirectory', [realpath($path . $exportPath)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
/**
|
||||
* Internal property used to determine whether the billable field should be calculated automatically.
|
||||
*/
|
||||
private string $billableMode = self::BILLABLE_DEFAULT;
|
||||
#[Assert\NotNull]
|
||||
private ?string $billableMode = self::BILLABLE_DEFAULT;
|
||||
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
||||
#[Assert\NotNull]
|
||||
private ?string $category = self::WORK;
|
||||
@@ -552,12 +553,12 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillableMode(): string
|
||||
public function getBillableMode(): ?string
|
||||
{
|
||||
return $this->billableMode;
|
||||
}
|
||||
|
||||
public function setBillableMode(string $billableMode): void
|
||||
public function setBillableMode(?string $billableMode): void
|
||||
{
|
||||
$this->billableMode = $billableMode;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
private ?bool $isAllowedToSeeAllData = null;
|
||||
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
|
||||
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Assert\Length(min: 2, max: 60, groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
|
||||
@@ -11,28 +11,56 @@ namespace App\Export;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Event\ExportItemsQueryEvent;
|
||||
use App\Export\Renderer\HtmlRendererFactory;
|
||||
use App\Export\Renderer\PdfRendererFactory;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class ServiceExport
|
||||
{
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $documentDirs = [];
|
||||
/**
|
||||
* @var ExportRendererInterface[]
|
||||
*/
|
||||
private $renderer = [];
|
||||
private array $renderer = [];
|
||||
/**
|
||||
* @var TimesheetExportInterface[]
|
||||
*/
|
||||
private $timesheetExporter = [];
|
||||
private array $timesheetExporter = [];
|
||||
/**
|
||||
* @var ExportRepositoryInterface[]
|
||||
*/
|
||||
private $repositories = [];
|
||||
private array $repositories = [];
|
||||
|
||||
public function __construct(private EventDispatcherInterface $eventDispatcher)
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private HtmlRendererFactory $htmlRendererFactory,
|
||||
private PdfRendererFactory $pdfRendererFactory
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function addDirectory(string $directory): void
|
||||
{
|
||||
$this->documentDirs[] = $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @CloudRequired
|
||||
*/
|
||||
public function removeDirectory(string $directory): void
|
||||
{
|
||||
if (($key = array_search($directory, $this->documentDirs, true)) !== false) {
|
||||
unset($this->documentDirs[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function addRenderer(ExportRendererInterface $renderer): void
|
||||
{
|
||||
$this->renderer[] = $renderer;
|
||||
@@ -43,12 +71,44 @@ final class ServiceExport
|
||||
*/
|
||||
public function getRenderer(): array
|
||||
{
|
||||
return $this->renderer;
|
||||
$renderer = [];
|
||||
|
||||
foreach ($this->documentDirs as $exportPath) {
|
||||
if (!is_dir($exportPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$htmlTemplates = glob($exportPath . '/*.html.twig');
|
||||
if (\is_array($htmlTemplates)) {
|
||||
foreach ($htmlTemplates as $htmlTpl) {
|
||||
$tplName = basename($htmlTpl);
|
||||
if (stripos($tplName, '-bundle') !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$renderer[] = $this->htmlRendererFactory->create($tplName, $tplName);
|
||||
}
|
||||
}
|
||||
|
||||
$pdfTemplates = glob($exportPath . '/*.pdf.twig');
|
||||
if (\is_array($pdfTemplates)) {
|
||||
foreach ($pdfTemplates as $pdfTpl) {
|
||||
$tplName = basename($pdfTpl);
|
||||
if (stripos($tplName, '-bundle') !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$renderer[] = $this->pdfRendererFactory->create($tplName, $tplName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($this->renderer, $renderer);
|
||||
}
|
||||
|
||||
public function getRendererById(string $id): ?ExportRendererInterface
|
||||
{
|
||||
foreach ($this->renderer as $renderer) {
|
||||
foreach ($this->getRenderer() as $renderer) {
|
||||
if ($renderer->getId() === $id) {
|
||||
return $renderer;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
$extendedOptions['data-disable-search'] = 1;
|
||||
}
|
||||
|
||||
// there is a very weird logic in vendor/symfony/twig-bridge/Resources/views/Form/form_div_layout.html.twig
|
||||
// in block "block choice_widget_collapsed" that resets "{% set required = false %}", so we fake it into the select
|
||||
if (true === $options['required'] && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
|
||||
$extendedOptions['required'] = 'required';
|
||||
$extendedOptions['placeholder'] = '';
|
||||
}
|
||||
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], $extendedOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class ProjectEditForm extends AbstractType
|
||||
$customer = null;
|
||||
$isNew = false;
|
||||
$options['currency'] = null;
|
||||
$customerOptions = [];
|
||||
|
||||
if (isset($options['data'])) {
|
||||
/** @var Project $entry */
|
||||
@@ -40,6 +41,11 @@ class ProjectEditForm extends AbstractType
|
||||
if (null !== $entry->getCustomer()) {
|
||||
$customer = $entry->getCustomer();
|
||||
$options['currency'] = $customer->getCurrency();
|
||||
|
||||
if (!$customer->isVisible()) {
|
||||
// force visibility, see https://github.com/kimai/kimai/issues/3985
|
||||
$customerOptions['pre_select_customer'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,11 +89,11 @@ class ProjectEditForm extends AbstractType
|
||||
'required' => false,
|
||||
'force_time' => 'end',
|
||||
]))
|
||||
->add('customer', CustomerType::class, [
|
||||
->add('customer', CustomerType::class, array_merge([
|
||||
'placeholder' => ($isNew && null === $customer) ? '' : false,
|
||||
'customers' => $customer,
|
||||
'query_builder_for_user' => true,
|
||||
])
|
||||
], $customerOptions))
|
||||
->add('globalActivities', YesNoType::class, [
|
||||
'label' => 'globalActivities',
|
||||
'help' => 'help.globalActivities'
|
||||
|
||||
@@ -133,19 +133,13 @@ trait ToolbarFormTrait
|
||||
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'visible'): void
|
||||
{
|
||||
$builder->add('visibility', VisibilityType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'label' => $label,
|
||||
'search' => false
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addPageSizeChoice(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder->add('pageSize', PageSizeType::class, [
|
||||
'required' => false,
|
||||
'search' => false
|
||||
]);
|
||||
$builder->add('pageSize', PageSizeType::class);
|
||||
}
|
||||
|
||||
protected function addUserRoleChoice(FormBuilderInterface $builder): void
|
||||
@@ -332,7 +326,6 @@ trait ToolbarFormTrait
|
||||
'asc' => BaseQuery::ORDER_ASC,
|
||||
'desc' => BaseQuery::ORDER_DESC
|
||||
],
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -346,7 +339,6 @@ trait ToolbarFormTrait
|
||||
$builder->add('orderBy', ChoiceType::class, [
|
||||
'label' => 'orderBy',
|
||||
'choices' => $all,
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -367,9 +359,6 @@ trait ToolbarFormTrait
|
||||
{
|
||||
$builder->add('state', ChoiceType::class, [
|
||||
'label' => 'entryState',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'search' => false,
|
||||
'choices' => [
|
||||
'all' => TimesheetQuery::STATE_ALL,
|
||||
'entryState.running' => TimesheetQuery::STATE_RUNNING,
|
||||
@@ -382,9 +371,6 @@ trait ToolbarFormTrait
|
||||
{
|
||||
$builder->add('exported', ChoiceType::class, [
|
||||
'label' => 'exported',
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'search' => false,
|
||||
'choices' => [
|
||||
'all' => TimesheetQuery::STATE_ALL,
|
||||
'yes' => TimesheetQuery::STATE_EXPORTED,
|
||||
@@ -395,10 +381,6 @@ trait ToolbarFormTrait
|
||||
|
||||
protected function addBillableChoice(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder->add('billable', BillableSearchType::class, [
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'search' => false,
|
||||
]);
|
||||
$builder->add('billable', BillableSearchType::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,17 @@ final class CustomerType extends AbstractType
|
||||
'ignore_customer' => null,
|
||||
// @var Customer|Customer[]|null
|
||||
'customers' => null,
|
||||
'pre_select_customer' => false,
|
||||
]);
|
||||
|
||||
$resolver->setDefault('query_builder', function (Options $options) {
|
||||
return function (CustomerRepository $repo) use ($options) {
|
||||
$query = new CustomerFormTypeQuery($options['customers']);
|
||||
|
||||
if (true === $options['pre_select_customer']) {
|
||||
$query->setAllowCustomerPreselect(true);
|
||||
}
|
||||
|
||||
if (true === $options['query_builder_for_user']) {
|
||||
$query->setUser($options['user']);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ final class InvoiceCalculatorType extends AbstractType
|
||||
return $renderer;
|
||||
},
|
||||
'translation_domain' => 'invoice-calculator',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ final class InvoiceRendererType extends AbstractType
|
||||
};
|
||||
},
|
||||
'translation_domain' => 'invoice-renderer',
|
||||
'search' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ final class SkinType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'search' => false,
|
||||
'required' => true,
|
||||
'choices' => self::THEMES,
|
||||
]);
|
||||
|
||||
@@ -45,7 +45,7 @@ final class InvoiceDocumentRepository
|
||||
*/
|
||||
public function removeDirectory(string $directory): void
|
||||
{
|
||||
if (($key = array_search($directory, $this->documentDirs)) !== false) {
|
||||
if (($key = array_search($directory, $this->documentDirs, true)) !== false) {
|
||||
unset($this->documentDirs[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +481,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertNotFalse($content);
|
||||
|
||||
$imgUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
||||
$imgUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||
$this->assertStringContainsString('<img src="' . $imgUrl . '" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" />', $content);
|
||||
|
||||
$formUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
||||
@@ -525,14 +525,14 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testIsTwoFactorImageSecure(): void
|
||||
{
|
||||
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
||||
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||
}
|
||||
|
||||
public function testTwoFactorImageFailsOnMissingSecret(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||
$this->assertRouteNotFound($client);
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
// this is required, so the totp secret is stored in the user entity
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
||||
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp.png');
|
||||
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/totp-qr-code');
|
||||
self::assertTrue($client->getResponse()->isSuccessful());
|
||||
self::assertEquals('image/png', $client->getResponse()->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest
|
||||
$this->assertStringContainsString('<input type="email"', $content);
|
||||
$this->assertStringContainsString('id="user_registration_form_email" name="user_registration_form[email]" required="required"', $content);
|
||||
$this->assertStringContainsString('<input type="text"', $content);
|
||||
$this->assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="60" pattern=".{2,}"', $content);
|
||||
$this->assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="60" pattern="', $content);
|
||||
$this->assertStringContainsString('<input type="password"', $content);
|
||||
$this->assertStringContainsString('id="user_registration_form_plainPassword_first" name="user_registration_form[plainPassword][first]" required="required"', $content);
|
||||
$this->assertStringContainsString('id="user_registration_form_plainPassword_second" name="user_registration_form[plainPassword][second]" required="required"', $content);
|
||||
|
||||
@@ -29,7 +29,9 @@ class ExportServiceCompilerPassTest extends TestCase
|
||||
private function getContainer(): ContainerBuilder
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kimai.export.documents', []); // TODO we could test that as well
|
||||
$container->setParameter('kimai.export.documents', [
|
||||
'templates/export/renderer/',
|
||||
]);
|
||||
|
||||
$definition = new Definition(ServiceExport::class);
|
||||
$container->setDefinition(ServiceExport::class, $definition);
|
||||
@@ -61,7 +63,8 @@ class ExportServiceCompilerPassTest extends TestCase
|
||||
$definition = $container->findDefinition(ServiceExport::class);
|
||||
$methods = $definition->getMethodCalls();
|
||||
|
||||
self::assertCount(5, $methods);
|
||||
self::assertCount(6, $methods);
|
||||
self::assertTrue($definition->hasMethodCall('addDirectory'));
|
||||
self::assertTrue($definition->hasMethodCall('addRenderer'));
|
||||
self::assertTrue($definition->hasMethodCall('addTimesheetExporter'));
|
||||
self::assertTrue($definition->hasMethodCall('addExportRepository'));
|
||||
|
||||
@@ -16,6 +16,8 @@ use App\Export\ServiceExport;
|
||||
use App\Export\Timesheet\HtmlRenderer as HtmlExporter;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\ExportQuery;
|
||||
use App\Tests\Mocks\Export\HtmlRendererFactoryMock;
|
||||
use App\Tests\Mocks\Export\PdfRendererFactoryMock;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -28,9 +30,11 @@ class ServiceExportTest extends TestCase
|
||||
{
|
||||
private function createSut(): ServiceExport
|
||||
{
|
||||
$sut = new ServiceExport($this->createMock(EventDispatcherInterface::class));
|
||||
|
||||
return $sut;
|
||||
return new ServiceExport(
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
(new HtmlRendererFactoryMock($this))->create(),
|
||||
(new PdfRendererFactoryMock($this))->create(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmptyObject()
|
||||
|
||||
30
tests/Mocks/Export/HtmlRendererFactoryMock.php
Normal file
30
tests/Mocks/Export/HtmlRendererFactoryMock.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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\Mocks\Export;
|
||||
|
||||
use App\Activity\ActivityStatisticService;
|
||||
use App\Export\Renderer\HtmlRendererFactory;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Tests\Mocks\AbstractMockFactory;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
class HtmlRendererFactoryMock extends AbstractMockFactory
|
||||
{
|
||||
public function create(): HtmlRendererFactory
|
||||
{
|
||||
return new HtmlRendererFactory(
|
||||
$this->createMock(Environment::class),
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(ProjectStatisticService::class),
|
||||
$this->createMock(ActivityStatisticService::class)
|
||||
);
|
||||
}
|
||||
}
|
||||
28
tests/Mocks/Export/PdfRendererFactoryMock.php
Normal file
28
tests/Mocks/Export/PdfRendererFactoryMock.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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\Mocks\Export;
|
||||
|
||||
use App\Export\Renderer\PdfRendererFactory;
|
||||
use App\Pdf\HtmlToPdfConverter;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Tests\Mocks\AbstractMockFactory;
|
||||
use Twig\Environment;
|
||||
|
||||
class PdfRendererFactoryMock extends AbstractMockFactory
|
||||
{
|
||||
public function create(): PdfRendererFactory
|
||||
{
|
||||
return new PdfRendererFactory(
|
||||
$this->createMock(Environment::class),
|
||||
$this->createMock(HtmlToPdfConverter::class),
|
||||
$this->createMock(ProjectStatisticService::class),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user