Release 2.46 (#5757)

This commit is contained in:
Kevin Papst
2026-01-07 00:59:47 +01:00
committed by GitHub
parent 9e87fc131b
commit 6a86afb5fd
55 changed files with 1582 additions and 1195 deletions

View File

@@ -327,7 +327,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
return [
'id' => 'int',
'name' => 'string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'visible' => 'bool',
];
@@ -358,7 +359,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'enabled' => 'bool',
'apiToken' => 'bool',
'systemAccount' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'avatar' => '@string',
'alias' => '@string',
'accountNumber' => '@string',
@@ -378,7 +380,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'enabled' => 'bool',
'apiToken' => 'bool',
'systemAccount' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'avatar' => '@string',
'alias' => '@string',
'accountNumber' => '@string',
@@ -402,7 +405,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
return [
'id' => 'int',
'name' => 'string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
];
// explicitly requested team
@@ -410,7 +414,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
return [
'id' => 'int',
'name' => 'string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'members' => ['result' => 'array', 'type' => 'TeamMember'],
// TODO more info in entity than in collection
'customers' => ['result' => 'array', 'type' => '@Customer'],
@@ -439,7 +444,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'bool',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'number' => '@string',
'comment' => '@string',
'currency' => 'string',
@@ -460,7 +466,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'boolean',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'number' => '@string',
'comment' => '@string',
'currency' => 'string',
@@ -482,7 +489,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'bool',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'number' => '@string',
'comment' => '@string',
'currency' => 'string',
@@ -519,7 +527,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'bool',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'customer' => 'int',
'number' => '@string',
'orderNumber' => '@string',
@@ -538,7 +547,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'bool',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'customer' => ['result' => 'object', 'type' => 'Customer'],
'number' => '@string',
'orderNumber' => '@string',
@@ -557,7 +567,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'bool',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'customer' => 'int',
'number' => '@string',
'orderNumber' => '@string',
@@ -578,7 +589,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'name' => 'string',
'visible' => 'bool',
'billable' => 'bool',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'customer' => 'int',
'number' => '@string',
'orderNumber' => '@string',
@@ -605,7 +617,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'billable' => 'bool',
'project' => '@int',
'number' => '@string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
'comment' => '@string',
];
@@ -618,7 +631,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'billable' => 'bool',
'project' => ['result' => 'object', 'type' => '@ProjectExpanded'],
'number' => '@string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
'comment' => '@string',
];
@@ -632,7 +646,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'billable' => 'bool',
'project' => '@int',
'number' => '@string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
'comment' => '@string',
'parentTitle' => '@string',
@@ -648,7 +663,8 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
'billable' => 'bool',
'project' => '@int',
'number' => '@string',
'color' => 'string',
'color' => '@string',
'color-safe' => 'string',
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
'comment' => '@string',
'parentTitle' => '@string',

View File

@@ -244,7 +244,6 @@ class ActivityControllerTest extends APIControllerBaseTestCase
self::assertIsArray($result);
self::assertApiResponseTypeStructure('ActivityEntity', $result);
self::assertCount(14, array_keys($result));
self::assertNull($result['parentTitle']);
self::assertNotEmpty($result['id']);
self::assertIsArray($result['teams']);
@@ -258,7 +257,8 @@ class ActivityControllerTest extends APIControllerBaseTestCase
self::assertNull($result['budgetType']);
self::assertNull($result['number']);
self::assertEquals('Test comment', $result['comment']);
self::assertEquals('#5319e7', $result['color']);
self::assertNull($result['color']);
self::assertEquals('#5319e7', $result['color-safe']);
self::assertTrue($result['visible']);
self::assertTrue($result['billable']);
}
@@ -297,7 +297,7 @@ class ActivityControllerTest extends APIControllerBaseTestCase
self::assertEquals('Test', $result['parentTitle']);
self::assertNotEmpty($result['id']);
self::assertIsArray($result['teams']);
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null, 'color-safe' => '#03A9F4']], $result['teams']);
self::assertIsArray($result['metaFields']);
self::assertEquals([], $result['metaFields']);
self::assertEquals('foo', $result['name']);

View File

@@ -229,7 +229,6 @@ class CustomerControllerTest extends APIControllerBaseTestCase
self::assertIsArray($result);
self::assertApiResponseTypeStructure('CustomerEntity', $result);
self::assertCount(30, array_keys($result));
self::assertNotEmpty($result['id']);
self::assertIsArray($result['teams']);
self::assertCount(1, $result['teams']);
@@ -262,7 +261,8 @@ class CustomerControllerTest extends APIControllerBaseTestCase
self::assertNull($result['homepage']);
self::assertEquals('Europe/Berlin', $result['timezone']);
self::assertNull($result['buyerReference']);
self::assertEquals('#5319e7', $result['color']);
self::assertNull($result['color']);
self::assertEquals('#5319e7', $result['color-safe']);
self::assertTrue($result['visible']);
self::assertTrue($result['billable']);
}
@@ -316,7 +316,7 @@ class CustomerControllerTest extends APIControllerBaseTestCase
self::assertApiResponseTypeStructure('CustomerEntity', $result);
self::assertNotEmpty($result['id']);
self::assertIsArray($result['teams']);
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null, 'color-safe' => '#03A9F4']], $result['teams']);
self::assertIsArray($result['metaFields']);
self::assertEquals([], $result['metaFields']);
self::assertEquals('foo', $result['name']);

View File

@@ -311,7 +311,6 @@ class ProjectControllerTest extends APIControllerBaseTestCase
self::assertIsArray($result);
self::assertApiResponseTypeStructure('ProjectEntity', $result);
self::assertCount(19, array_keys($result));
self::assertEquals('first one', $result['parentTitle']);
self::assertEquals($project->getId(), $result['id']);
self::assertIsArray($result['teams']);
@@ -329,7 +328,8 @@ class ProjectControllerTest extends APIControllerBaseTestCase
self::assertNull($result['orderNumber']);
self::assertNull($result['number']);
self::assertNull($result['comment']);
self::assertEquals('#2ECC40', $result['color']);
self::assertNull($result['color']);
self::assertEquals('#2ECC40', $result['color-safe']);
self::assertTrue($result['globalActivities']);
self::assertTrue($result['billable']);
self::assertTrue($result['visible']);
@@ -374,7 +374,7 @@ class ProjectControllerTest extends APIControllerBaseTestCase
self::assertEquals('Test', $result['parentTitle']);
self::assertNotEmpty($result['id']);
self::assertIsArray($result['teams']);
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => '#03A9F4']], $result['teams']);
self::assertEquals([['id' => 1, 'name' => 'Test team', 'color' => null, 'color-safe' => '#03A9F4']], $result['teams']);
self::assertIsArray($result['metaFields']);
self::assertEquals([], $result['metaFields']);
self::assertEquals('foo', $result['name']);

View File

@@ -0,0 +1,128 @@
<?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\Export\Base;
use App\Export\Base\CsvRenderer;
use App\Export\Base\HtmlRenderer;
use App\Export\Base\PDFRenderer;
use App\Export\Base\XlsxRenderer;
use App\Export\ServiceExport;
use App\Repository\ExportTemplateRepository;
use App\Tests\Export\Renderer\AbstractRendererTestCase;
use App\Tests\Mocks\Export\CsvRendererFactoryMock;
use App\Tests\Mocks\Export\HtmlRendererFactoryMock;
use App\Tests\Mocks\Export\PdfRendererFactoryMock;
use App\Tests\Mocks\Export\XlsxRendererFactoryMock;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use Psr\Log\LoggerInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
#[CoversClass(ServiceExport::class)]
#[CoversClass(CsvRenderer::class)]
#[CoversClass(XlsxRenderer::class)]
#[CoversClass(PDFRenderer::class)]
#[CoversClass(HtmlRenderer::class)]
#[Group('integration')]
class DefaultRendererTest extends AbstractRendererTestCase
{
private function createServiceExport(): ServiceExport
{
$repository = $this->createMock(ExportTemplateRepository::class);
$repository->expects($this->once())->method('findAll')->willReturn([]);
$logger = $this->createMock(LoggerInterface::class);
return new ServiceExport(
$this->createMock(EventDispatcherInterface::class),
(new HtmlRendererFactoryMock($this))->create(),
(new PdfRendererFactoryMock($this))->create(),
(new CsvRendererFactoryMock($this))->create(),
(new XlsxRendererFactoryMock($this))->create(),
$repository,
$logger,
);
}
public function testRenderDefaultTemplates(): void
{
$sut = $this->createServiceExport();
$renderer = $sut->getRenderer();
self::assertCount(4, $renderer);
self::assertInstanceOf(CsvRenderer::class, $renderer[0]);
self::assertInstanceOf(XlsxRenderer::class, $renderer[1]);
self::assertInstanceOf(PDFRenderer::class, $renderer[2]);
self::assertInstanceOf(HtmlRenderer::class, $renderer[3]);
// make sure that the default templates do NOT violate the Twig SecurityPolicy
$response = $this->render($renderer[0]);
self::assertEquals('text/csv', $response->headers->get('Content-Type'));
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition') ?? '');
$response = $this->render($renderer[1]);
self::assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type') ?? '');
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition') ?? '');
$response = $this->render($renderer[2]);
self::assertEquals('application/pdf', $response->headers->get('Content-Type') ?? '');
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition') ?? '');
$response = $this->render($renderer[3]);
self::assertEquals('text/html', $response->headers->get('Content-Type') ?? '');
// HTML is attached to the body and twig is a mock in this setupo, so we just receive an empty string
}
public function testRenderCustomTemplates(): void
{
$searchDir = __DIR__ . '/../../../var/templates';
if (!is_dir($searchDir)) {
$this->expectNotToPerformAssertions();
return;
}
$finder = new Finder();
$finder
->in($searchDir)
->name('*.twig')
->path('export-tpl/')
->files()
;
$files = [];
$dirs = [];
foreach ($finder->getIterator() as $filename => $splFile) {
$files[] = $splFile->getRealPath();
$dir = \dirname($splFile->getRealPath());
$dirs[$dir] = $dir;
}
$dirs = array_keys($dirs);
if (\count($dirs) === 0) {
$this->expectNotToPerformAssertions();
return;
}
$sut = $this->createServiceExport();
foreach ($dirs as $dir) {
$sut->addDirectory($dir);
}
$renderers = $sut->getRenderer();
self::assertCount(4 + \count($files), $renderers);
foreach ($renderers as $renderer) {
$response = $this->render($renderer);
self::assertInstanceOf(Response::class, $response);
}
}
}

View File

@@ -129,8 +129,8 @@ abstract class AbstractRendererTestCase extends KernelTestCase
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5, $timesheet6];
$currentUser = $this->createMock(User::class);
$currentUser->expects($this->any())->method('isExportDecimal')->willReturn($exportDecimal);
$currentUser = new User();
$currentUser->setPreferenceValue('export_decimal', $exportDecimal);
$query = new TimesheetQuery();
$query->setActivities([$activity]);

View File

@@ -14,10 +14,12 @@ use App\Invoice\InvoiceItemHydrator;
use App\Invoice\InvoiceModel;
use App\Invoice\InvoiceModelHydrator;
use App\Model\InvoiceDocument;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Response;
#[CoversClass(DebugRenderer::class)]
class DebugRendererTest extends TestCase
{
use RendererTestTrait;

View File

@@ -18,6 +18,7 @@ use App\Tests\Mocks\FileHelperFactory;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Environment;
@@ -118,12 +119,26 @@ class PdfRendererTest extends KernelTestCase
$dirs = [
__DIR__ . '/../../../templates/invoice/renderer/',
//__DIR__ . '/../../../var/invoices/',
//__DIR__ . '/../../../var/invoices_customer/',
//__DIR__ . '/../../../var/invoices_old/',
];
$files = [];
$additionalTemplatesDir = __DIR__ . '/../../../var/templates';
if (is_dir($additionalTemplatesDir)) {
$finder = new Finder();
$finder
->in($additionalTemplatesDir)
->name('*.pdf.twig')
->path('invoice-tpl/')
->files();
foreach ($finder->getIterator() as $splFile) {
$filename = $splFile->getRealPath();
$files[] = $filename;
$loader->addPath(\dirname($filename) . '/', 'invoice');
}
}
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
continue;
@@ -145,7 +160,6 @@ class PdfRendererTest extends KernelTestCase
$response = $sut->render($document, $model);
self::assertEquals('application/pdf', $response->headers->get('Content-Type'));
self::assertStringContainsString('attachment; filename', $response->headers->get('Content-Disposition'));
self::assertNotEmpty($response->getContent());
}
}
}

View File

@@ -32,7 +32,6 @@ use App\Model\InvoiceDocument;
use App\Repository\InvoiceRepository;
use App\Repository\Query\InvoiceQuery;
use App\Tests\Mocks\InvoiceModelFactoryFactory;
use Doctrine\Common\Collections\ArrayCollection;
trait RendererTestTrait
{
@@ -85,6 +84,10 @@ trait RendererTestTrait
protected function getInvoiceModel(): InvoiceModel
{
$activityId = new \ReflectionProperty(Activity::class, 'id');
$projectId = new \ReflectionProperty(Project::class, 'id');
$userId = new \ReflectionProperty(User::class, 'id');
$user = new User();
$user->setUserIdentifier('one-user');
$user->setTitle('user title');
@@ -107,59 +110,54 @@ trait RendererTestTrait
$pMeta = new ProjectMeta();
$pMeta->setName('foo-project')->setValue('bar-project')->setIsVisible(true);
$project = $this->createMock(Project::class);
$project->method('getId')->willReturn(0);
$project->method('getName')->willReturn('project name');
$project->method('getCustomer')->willReturn($customer);
$project->method('getMetaFields')->willReturn(new ArrayCollection([$pMeta]));
$project->method('getVisibleMetaFields')->willReturn([$pMeta]);
$project = new Project();
$projectId->setValue($project, 0);
$project->setName('project name');
$project->setCustomer($customer);
$project->setMetaField($pMeta);
$aMeta = new ActivityMeta();
$aMeta->setName('foo-activity');
$aMeta->setValue('bar-activity');
$aMeta->setIsVisible(true);
$activity = $this->createMock(Activity::class);
$activity->method('getId')->willReturn(0);
$activity->method('getName')->willReturn('activity description');
$activity->method('getProject')->willReturn($project);
$activity->method('getMetaFields')->willReturn(new ArrayCollection([$aMeta]));
$activity->method('getVisibleMetaFields')->willReturn([$aMeta]);
$activity = new Activity();
$activityId->setValue($activity, 0);
$activity->setName('activity description');
$activity->setProject($project);
$activity->setMetaField($aMeta);
$pMeta2 = new ProjectMeta();
$pMeta2->setName('foo-project')->setValue('bar-project2')->setIsVisible(true);
$project2 = $this->createMock(Project::class);
$project2->method('getId')->willReturn(1);
$project2->method('getName')->willReturn('project 2 name');
$project2->method('getCustomer')->willReturn($customer);
$project2->method('getMetaFields')->willReturn(new ArrayCollection([$pMeta2]));
$project2->method('getVisibleMetaFields')->willReturn([$pMeta2]);
$project2 = new Project();
$projectId->setValue($project2, 1);
$project2->setName('project 2 name');
$project2->setCustomer($customer);
$project2->setMetaField($pMeta2);
$aMeta2 = new ActivityMeta();
$aMeta2->setName('foo-activity');
$aMeta2->setValue('bar-activity2');
$aMeta2->setIsVisible(true);
$activity2 = $this->createMock(Activity::class);
$activity2->method('getId')->willReturn(1);
$activity2->method('getName')->willReturn('activity 1 description');
$activity2->method('getProject')->willReturn($project2);
$activity2->method('getMetaFields')->willReturn(new ArrayCollection([$aMeta2]));
$activity2->method('getVisibleMetaFields')->willReturn([$aMeta2]);
$activity2 = new Activity();
$activityId->setValue($activity2, 1);
$activity2->setName('activity 1 description');
$activity2->setProject($project2);
$activity2->setMetaField($aMeta2);
$pref1 = new UserPreference('foo', 'bar');
$pref2 = new UserPreference('mad', 123.45);
$userMethods = ['getId', 'getPreferenceValue', 'getVisiblePreferences', 'getUsername', 'getUserIdentifier'];
$user1 = $this->getMockBuilder(User::class)->onlyMethods($userMethods)->disableOriginalConstructor()->getMock();
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');
$user1->method('getUserIdentifier')->willReturn('foo-bar');
$user1->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
$user1 = new User();
$user1->setUserIdentifier('foo-bar');
$userId->setValue($user1, 1);
//$user1->method('getPreferenceValue')->willReturn('50');
$user1->addPreference($pref1);
$user1->addPreference($pref2);
$user2 = $this->createMock(User::class);
$user2->method('getId')->willReturn(2);
$user2->method('getUsername')->willReturn('hello-world');
$user2->method('getUserIdentifier')->willReturn('hello-world');
$user2->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
$user2 = new User();
$userId->setValue($user2, 2);
$user2->setUserIdentifier('hello-world');
$user2->addPreference($pref1);
$user2->addPreference($pref2);
$timesheet = new Timesheet();
$timesheet->setDuration(3600);
@@ -302,12 +300,14 @@ trait RendererTestTrait
$pref1 = new UserPreference('foo', 'bar');
$pref2 = new UserPreference('mad', 123.45);
$user1 = $this->createMock(User::class);
$user1->method('getId')->willReturn(1);
$user1->method('getPreferenceValue')->willReturn('50');
$user1->method('getUsername')->willReturn('foo-bar');
$user1->method('getUserIdentifier')->willReturn('foo-bar');
$user1->method('getVisiblePreferences')->willReturn([$pref1, $pref2]);
$userId = new \ReflectionProperty(User::class, 'id');
$user1 = new User();
$user1->setUserIdentifier('foo-bar');
$user1->addPreference($pref1);
$user1->addPreference($pref2);
$userId->setValue($user1, 1);
//$user1->method('getPreferenceValue')->willReturn('50');
$timesheet = new Timesheet();
$timesheet->setDuration(3600);

View File

@@ -0,0 +1,91 @@
<?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\Twig\SecurityPolicy;
use App\Entity\User;
use App\Pdf\PdfContext;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ServerBag;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\String\UnicodeString;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityPolicyInterface;
abstract class AbstractPolicyTestCase extends TestCase
{
abstract protected function createPolicy(): SecurityPolicyInterface;
public function testCheckSecurity(): void
{
$sut = $this->createPolicy();
$sut->checkSecurity([], [], []);
$this->expectNotToPerformAssertions();
}
public function testCheckPropertyAllowed(): void
{
$sut = $this->createPolicy();
$sut->checkPropertyAllowed(new \stdClass(), 'foo');
$this->expectNotToPerformAssertions();
}
#[DataProvider('getCheckMethodAllowedData')]
public function testCheckMethodAllowed(object $obj, string $method, ?string $expectedExceptionMessage = null): void
{
$sut = $this->createPolicy();
if ($expectedExceptionMessage !== null) {
$this->expectException(SecurityNotAllowedMethodError::class);
$this->expectExceptionMessage($expectedExceptionMessage);
}
$sut->checkMethodAllowed($obj, $method);
if ($expectedExceptionMessage === null) {
$this->expectNotToPerformAssertions();
}
}
public static function getCheckMethodAllowedData(): array
{
return [
[new ServerBag(), 'get', 'Tried to access server environment'],
[self::createStub(SessionInterface::class), 'getId', 'Tried to access session'],
[new \stdClass(), 'foo', 'Tried to access non-read method'],
[new \stdClass(), 'setFoo', 'Tried to access non-read method'],
[new \stdClass(), 'getFoo'],
[new \stdClass(), 'hasFoo'],
[new \stdClass(), 'isFoo'],
[new UnicodeString(), '__toString'],
// Request
[new Request(), 'get', null],
[new Request(), 'isXmlHttpRequest', 'Tried to call setter() of app variable'],
[new Request(), 'hasSession', 'Tried to call setter() of app variable'],
// PdfContext
[new PdfContext(), 'setOption'],
[new PdfContext(), 'getOption', 'Tried to access forbidden method on PdfContext'],
// AppVariable
[new AppVariable(), 'getRequest'],
[new AppVariable(), 'getUser'],
[new AppVariable(), 'getLocale'],
[new AppVariable(), 'getCharset', 'Tried to access forbidden app variable method'],
// User
[new User(), 'getUsername'],
[new User(), 'getPassword', 'Tried to access user secrets'],
[new User(), 'getTotpSecret', 'Tried to access user secrets'],
[new User(), 'getPlainPassword', 'Tried to access user secrets'],
[new User(), 'getConfirmationToken', 'Tried to access user secrets'],
[new User(), 'getTotpAuthenticationConfiguration', 'Tried to access user secrets'],
];
}
}

View File

@@ -0,0 +1,66 @@
<?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\Twig\SecurityPolicy;
use App\Twig\SecurityPolicy\ChainPolicy;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Twig\Sandbox\SecurityPolicyInterface;
#[CoversClass(ChainPolicy::class)]
class ChainPolicyTest extends TestCase
{
public function testCheckSecurity(): void
{
$policy1 = $this->createMock(SecurityPolicyInterface::class);
$policy1->expects(self::once())->method('checkSecurity')->with(['tag'], ['filter'], ['function']);
$policy2 = $this->createMock(SecurityPolicyInterface::class);
$policy2->expects(self::once())->method('checkSecurity')->with(['tag'], ['filter'], ['function']);
$sut = new ChainPolicy();
$sut->addPolicy($policy1);
$sut->addPolicy($policy2);
$sut->checkSecurity(['tag'], ['filter'], ['function']);
}
public function testCheckMethodAllowed(): void
{
$obj = new \stdClass();
$policy1 = $this->createMock(SecurityPolicyInterface::class);
$policy1->expects(self::once())->method('checkMethodAllowed')->with($obj, 'method');
$policy2 = $this->createMock(SecurityPolicyInterface::class);
$policy2->expects(self::once())->method('checkMethodAllowed')->with($obj, 'method');
$sut = new ChainPolicy();
$sut->addPolicy($policy1);
$sut->addPolicy($policy2);
$sut->checkMethodAllowed($obj, 'method');
}
public function testCheckPropertyAllowed(): void
{
$obj = new \stdClass();
$policy1 = $this->createMock(SecurityPolicyInterface::class);
$policy1->expects(self::once())->method('checkPropertyAllowed')->with($obj, 'property');
$policy2 = $this->createMock(SecurityPolicyInterface::class);
$policy2->expects(self::once())->method('checkPropertyAllowed')->with($obj, 'property');
$sut = new ChainPolicy();
$sut->addPolicy($policy1);
$sut->addPolicy($policy2);
$sut->checkPropertyAllowed($obj, 'property');
}
}

View File

@@ -0,0 +1,23 @@
<?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\Twig\SecurityPolicy;
use App\Twig\SecurityPolicy\DefaultPolicy;
use PHPUnit\Framework\Attributes\CoversClass;
use Twig\Sandbox\SecurityPolicyInterface;
#[CoversClass(DefaultPolicy::class)]
class DefaultPolicyTest extends AbstractPolicyTestCase
{
protected function createPolicy(): SecurityPolicyInterface
{
return new DefaultPolicy();
}
}

View File

@@ -0,0 +1,23 @@
<?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\Twig\SecurityPolicy;
use App\Twig\SecurityPolicy\ExportPolicy;
use PHPUnit\Framework\Attributes\CoversClass;
use Twig\Sandbox\SecurityPolicyInterface;
#[CoversClass(ExportPolicy::class)]
class ExportPolicyTest extends AbstractPolicyTestCase
{
protected function createPolicy(): SecurityPolicyInterface
{
return new ExportPolicy();
}
}

View File

@@ -0,0 +1,90 @@
<?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\Twig\SecurityPolicy;
use App\Entity\User;
use App\Pdf\PdfContext;
use App\Twig\SecurityPolicy\InvoicePolicy;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ServerBag;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\String\UnicodeString;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityPolicyInterface;
#[CoversClass(InvoicePolicy::class)]
class InvoicePolicyTest extends TestCase
{
protected function createPolicy(): SecurityPolicyInterface
{
return new InvoicePolicy();
}
public function testCheckSecurity(): void
{
$sut = $this->createPolicy();
$sut->checkSecurity([], [], []);
$this->expectNotToPerformAssertions();
}
#[DataProvider('getCheckMethodAllowedData')]
public function testCheckMethodAllowed(object $obj, string $method, ?string $expectedExceptionMessage = null): void
{
$sut = $this->createPolicy();
if ($expectedExceptionMessage !== null) {
$this->expectException(SecurityNotAllowedMethodError::class);
$this->expectExceptionMessage($expectedExceptionMessage);
}
$sut->checkMethodAllowed($obj, $method);
if ($expectedExceptionMessage === null) {
$this->expectNotToPerformAssertions();
}
}
public static function getCheckMethodAllowedData(): array
{
return [
[new ServerBag(), 'get', 'Tried to access server environment'],
[self::createStub(SessionInterface::class), 'getId', 'Tried to access session'],
[new \stdClass(), 'foo', 'Tried to access non-read method'],
[new \stdClass(), 'setFoo', 'Tried to access non-read method'],
[new \stdClass(), 'getFoo'],
[new \stdClass(), 'hasFoo'],
[new \stdClass(), 'isFoo'],
[new UnicodeString(), '__toString'],
// Request
[new Request(), 'get', null],
[new Request(), 'isXmlHttpRequest', 'Tried to call setter() of app variable'],
[new Request(), 'hasSession', 'Tried to call setter() of app variable'],
// PdfContext
[new PdfContext(), 'setOption'],
[new PdfContext(), 'getOption', 'Tried to access forbidden method on PdfContext'],
// AppVariable
[new AppVariable(), 'getRequest'],
[new AppVariable(), 'getUser'],
[new AppVariable(), 'getLocale'],
[new AppVariable(), 'getCharset', 'Tried to access forbidden app variable method'],
// User
[new User(), 'getUsername'],
[new User(), 'getPassword', 'Tried to access user secrets'],
[new User(), 'getTotpSecret', 'Tried to access user secrets'],
[new User(), 'getPlainPassword', 'Tried to access user secrets'],
[new User(), 'getConfirmationToken', 'Tried to access user secrets'],
[new User(), 'getTotpAuthenticationConfiguration', 'Tried to access user secrets'],
];
}
}