new API endpoint to save invoice meta-fields (#5916)
This commit is contained in:
@@ -136,7 +136,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertEntityNotFoundForPatch(string $role, string $url, array $data): void
|
||||
protected function assertEntityNotFoundForPatch(HttpKernelBrowser|string $role, string $url, array $data): void
|
||||
{
|
||||
$this->assertExceptionForPatchAction($role, $url, $data, [
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
@@ -152,17 +152,17 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertExceptionForDeleteAction(string $role, string $url, array $data, array $expectedErrors): void
|
||||
protected function assertExceptionForDeleteAction(HttpKernelBrowser|string $role, string $url, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->assertExceptionForRole($role, $url, 'DELETE', $data, $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertExceptionForPatchAction(string $role, string $url, array $data, array $expectedErrors): void
|
||||
protected function assertExceptionForPatchAction(HttpKernelBrowser|string $role, string $url, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->assertExceptionForRole($role, $url, 'PATCH', $data, $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertExceptionForPostAction(string $role, string $url, array $data, array $expectedErrors): void
|
||||
protected function assertExceptionForPostAction(HttpKernelBrowser|string $role, string $url, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->assertExceptionForRole($role, $url, 'POST', $data, $expectedErrors);
|
||||
}
|
||||
@@ -180,9 +180,9 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
self::assertEquals($expectedErrors, json_decode($response->getContent(), true));
|
||||
}
|
||||
|
||||
protected function assertExceptionForRole(string $role, string $url, string $method, array $data, array $expectedErrors): void
|
||||
protected function assertExceptionForRole(HttpKernelBrowser|string $role, string $url, string $method, array $data, array $expectedErrors): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$client = ($role instanceof HttpKernelBrowser) ? $role : $this->getClientForAuthenticatedUser($role);
|
||||
$this->assertExceptionForMethod($client, $url, $method, $data, $expectedErrors);
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'total' => 'float',
|
||||
'vat' => 'float',
|
||||
'overdue' => 'bool',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'CustomerMeta'],
|
||||
'metaFields' => ['result' => 'array', 'type' => 'InvoiceMeta'],
|
||||
];
|
||||
|
||||
case 'PageActionItem':
|
||||
@@ -350,6 +350,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'value' => '@string',
|
||||
];
|
||||
|
||||
case 'InvoiceMeta':
|
||||
case 'CustomerMeta':
|
||||
case 'ProjectMeta':
|
||||
case 'ActivityMeta':
|
||||
@@ -630,7 +631,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ActivityMeta'], // since 2.45
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
@@ -644,7 +645,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'], // since 2.45
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ActivityMeta'], // since 2.45
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
@@ -659,7 +660,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ActivityMeta'],
|
||||
'comment' => '@string',
|
||||
'parentTitle' => '@string',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
@@ -676,7 +677,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'number' => '@string',
|
||||
'color' => '@string',
|
||||
'color-safe' => 'string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ActivityMeta'],
|
||||
'comment' => '@string',
|
||||
'parentTitle' => '@string',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
|
||||
@@ -76,6 +76,7 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
|
||||
'/api/export/{id}',
|
||||
'/api/invoices',
|
||||
'/api/invoices/{id}',
|
||||
'/api/invoices/{id}/custom-fields',
|
||||
'/api/projects',
|
||||
'/api/projects/{id}',
|
||||
'/api/projects/{id}/meta',
|
||||
|
||||
@@ -15,13 +15,17 @@ use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Tests\DataFixtures\InvoiceFixtures;
|
||||
use App\Tests\Mocks\InvoiceTestMetaFieldSubscriberMock;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
#[Group('integration')]
|
||||
class InvoiceControllerTest extends APIControllerBaseTestCase
|
||||
{
|
||||
/**
|
||||
* @return Invoice[]
|
||||
* @param int<1, 999> $amount
|
||||
* @return non-empty-array<Invoice>
|
||||
*/
|
||||
protected function importInvoiceFixtures(int $amount, ?array $status = null): array
|
||||
{
|
||||
@@ -120,6 +124,8 @@ class InvoiceControllerTest extends APIControllerBaseTestCase
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('Invoice', $result);
|
||||
self::assertArrayHasKey('metaFields', $result);
|
||||
self::assertCount(0, $result['metaFields']);
|
||||
}
|
||||
|
||||
public function testNotFound(): void
|
||||
@@ -181,4 +187,95 @@ class InvoiceControllerTest extends APIControllerBaseTestCase
|
||||
$this->request($client, '/api/invoices', 'GET', $query);
|
||||
$this->assertApiResponseAccessDenied($client->getResponse());
|
||||
}
|
||||
|
||||
// ------------------------------------- [META FIELDS] -------------------------------------
|
||||
|
||||
public function testUpdateInvoiceMetaFieldsThrowsNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/invoices/42/custom-fields', []);
|
||||
}
|
||||
|
||||
public function testUpdateInvoiceMetaFieldsThrowsExceptionOnWrongStructure(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$id = $this->importInvoiceFixtures(1)[0]->getId();
|
||||
|
||||
$this->assertExceptionForPatchAction($client, '/api/invoices/' . $id . '/custom-fields', ['name' => 'X', 'value' => 'X'], [
|
||||
'code' => Response::HTTP_BAD_REQUEST,
|
||||
'message' => 'Bad Request'
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateInvoiceMetaFieldsThrowsExceptionOnMissingName(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$id = $this->importInvoiceFixtures(1)[0]->getId();
|
||||
|
||||
$this->assertExceptionForPatchAction($client, '/api/invoices/' . $id . '/custom-fields', [['value' => 'X']], [
|
||||
'code' => Response::HTTP_BAD_REQUEST,
|
||||
'message' => 'Bad Request'
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateInvoiceMetaFieldsThrowsExceptionOnMissingValue(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$id = $this->importInvoiceFixtures(1)[0]->getId();
|
||||
|
||||
$this->assertExceptionForPatchAction($client, '/api/invoices/' . $id . '/custom-fields', [['name' => 'X']], [
|
||||
'code' => Response::HTTP_BAD_REQUEST,
|
||||
'message' => 'Bad Request'
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateInvoiceMetaFieldsThrowsExceptionOnMissingMetafield(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$id = $this->importInvoiceFixtures(1)[0]->getId();
|
||||
|
||||
$this->assertExceptionForPatchAction($client, '/api/invoices/' . $id . '/custom-fields', [['name' => 'X', 'value' => 'Y']], [
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
'message' => 'Not Found'
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateInvoiceMetaFields(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$invoices = $this->importInvoiceFixtures(1);
|
||||
$id = $invoices[0]->getId();
|
||||
/** @var EventDispatcher $dispatcher */
|
||||
$dispatcher = static::getContainer()->get('event_dispatcher');
|
||||
$dispatcher->addSubscriber(new InvoiceTestMetaFieldSubscriberMock());
|
||||
|
||||
$data = [
|
||||
[
|
||||
'name' => 'metatestmock',
|
||||
'value' => 'another,testing,bar'
|
||||
],
|
||||
[
|
||||
'name' => 'foobar',
|
||||
'value' => 13081978
|
||||
],
|
||||
];
|
||||
$this->request($client, '/api/invoices/' . $id . '/custom-fields', 'PATCH', [], (string) json_encode($data));
|
||||
|
||||
self::assertTrue($client->getResponse()->isSuccessful());
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('Invoice', $result);
|
||||
self::assertArrayHasKey('metaFields', $result);
|
||||
// only visible should be returned
|
||||
self::assertCount(1, $result['metaFields']);
|
||||
self::assertEquals(['name' => 'metatestmock', 'value' => 'another,testing,bar'], $result['metaFields'][0]);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
/** @var Invoice $invoice */
|
||||
$invoice = $em->getRepository(Invoice::class)->find($id);
|
||||
self::assertEquals('another,testing,bar', $invoice->getMetaField('metatestmock')?->getValue());
|
||||
self::assertEquals(13081978, $invoice->getMetaField('foobar')?->getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Command\InvoiceCreateCommand;
|
||||
use App\DataFixtures\UserFixtures;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Invoice\InvoiceService;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceTemplateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -67,7 +67,7 @@ class InvoiceCreateCommandTest extends KernelTestCase
|
||||
$container = self::getContainer();
|
||||
|
||||
$this->application->add(new InvoiceCreateCommand(
|
||||
$container->get(ServiceInvoice::class), // @phpstan-ignore argument.type
|
||||
$container->get(InvoiceService::class), // @phpstan-ignore argument.type
|
||||
$container->get(CustomerRepository::class), // @phpstan-ignore argument.type
|
||||
$container->get(ProjectRepository::class), // @phpstan-ignore argument.type
|
||||
$container->get(InvoiceTemplateRepository::class), // @phpstan-ignore argument.type
|
||||
|
||||
@@ -16,6 +16,7 @@ use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Activity>
|
||||
*/
|
||||
final class ActivityFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Customer>
|
||||
*/
|
||||
final class CustomerFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace App\Tests\DataFixtures;
|
||||
use App\Entity\ExportTemplate;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* @implements TestFixture<ExportTemplate>
|
||||
*/
|
||||
final class ExportTemplateFixtures implements TestFixture
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Invoice>
|
||||
*/
|
||||
class InvoiceFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<InvoiceTemplate>
|
||||
*/
|
||||
class InvoiceTemplateFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Project>
|
||||
*/
|
||||
final class ProjectFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Tag>
|
||||
*/
|
||||
final class TagFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Team>
|
||||
*/
|
||||
final class TeamFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -13,11 +13,14 @@ use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @template TEntity
|
||||
*/
|
||||
interface TestFixture
|
||||
{
|
||||
/**
|
||||
* Load data fixtures with the passed EntityManager and returns the created objects.
|
||||
*
|
||||
* @return non-empty-array<TEntity>
|
||||
*/
|
||||
public function load(ObjectManager $manager): array;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in during controller tests.
|
||||
* @implements TestFixture<Timesheet>
|
||||
*/
|
||||
final class TimesheetFixtures implements TestFixture
|
||||
{
|
||||
|
||||
@@ -15,12 +15,12 @@ use App\Invoice\Calculator\ShortInvoiceCalculator;
|
||||
use App\Invoice\Calculator\UserInvoiceCalculator;
|
||||
use App\Invoice\CalculatorInterface;
|
||||
use App\Invoice\InvoiceItemRepositoryInterface;
|
||||
use App\Invoice\InvoiceService;
|
||||
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\Repository\TimesheetInvoiceItemRepository;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -34,8 +34,8 @@ class InvoiceServiceCompilerPassTest extends TestCase
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$definition = new Definition(ServiceInvoice::class);
|
||||
$container->setDefinition(ServiceInvoice::class, $definition);
|
||||
$definition = new Definition(InvoiceService::class);
|
||||
$container->setDefinition(InvoiceService::class, $definition);
|
||||
|
||||
$renderers = [DocxRenderer::class];
|
||||
foreach ($renderers as $renderer) {
|
||||
@@ -66,7 +66,7 @@ class InvoiceServiceCompilerPassTest extends TestCase
|
||||
$sut = new InvoiceServiceCompilerPass();
|
||||
$sut->process($container);
|
||||
|
||||
$definition = $container->findDefinition(ServiceInvoice::class);
|
||||
$definition = $container->findDefinition(InvoiceService::class);
|
||||
$methods = $definition->getMethodCalls();
|
||||
|
||||
self::assertCount(7, $methods);
|
||||
|
||||
28
tests/Event/InvoiceUpdatePostEventTest.php
Normal file
28
tests/Event/InvoiceUpdatePostEventTest.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\Event;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Event\InvoiceUpdatePostEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(InvoiceUpdatePostEvent::class)]
|
||||
class InvoiceUpdatePostEventTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
$sut = new InvoiceUpdatePostEvent($invoice);
|
||||
|
||||
self::assertSame($invoice, $sut->getInvoice());
|
||||
}
|
||||
}
|
||||
28
tests/Event/InvoiceUpdatePreEventTest.php
Normal file
28
tests/Event/InvoiceUpdatePreEventTest.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\Event;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Event\InvoiceUpdatePreEvent;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(InvoiceUpdatePreEvent::class)]
|
||||
class InvoiceUpdatePreEventTest extends TestCase
|
||||
{
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
|
||||
$sut = new InvoiceUpdatePreEvent($invoice);
|
||||
|
||||
self::assertSame($invoice, $sut->getInvoice());
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Entity\Timesheet;
|
||||
use App\Invoice\Calculator\DefaultCalculator;
|
||||
use App\Invoice\InvoiceItemRepositoryInterface;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\InvoiceService;
|
||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||
use App\Invoice\Renderer\TwigRenderer;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
@@ -29,12 +30,14 @@ use App\Tests\Mocks\InvoiceModelFactoryFactory;
|
||||
use App\Utils\FileHelper;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
#[CoversClass(ServiceInvoice::class)]
|
||||
class ServiceInvoiceTest extends TestCase
|
||||
#[CoversClass(InvoiceService::class)]
|
||||
#[CoversClass(ServiceInvoice::class)] // @phpstan-ignore-line
|
||||
class InvoiceServiceTest extends TestCase
|
||||
{
|
||||
private function getSut(array $paths): ServiceInvoice
|
||||
private function getSut(array $paths): InvoiceService
|
||||
{
|
||||
$languages = [
|
||||
'en' => LocaleService::DEFAULT_SETTINGS
|
||||
@@ -45,7 +48,14 @@ class ServiceInvoiceTest extends TestCase
|
||||
$repo = new InvoiceDocumentRepository($paths);
|
||||
$invoiceRepo = $this->createMock(InvoiceRepository::class);
|
||||
|
||||
return new ServiceInvoice($repo, new FileHelper(realpath(__DIR__ . '/../../var/data/')), $invoiceRepo, $formattings, (new InvoiceModelFactoryFactory($this))->create());
|
||||
return new InvoiceService(
|
||||
$repo,
|
||||
new FileHelper(realpath(__DIR__ . '/../../var/data/')),
|
||||
$invoiceRepo,
|
||||
$formattings,
|
||||
(new InvoiceModelFactoryFactory($this))->create(),
|
||||
$this->createMock(EventDispatcherInterface::class)
|
||||
);
|
||||
}
|
||||
|
||||
public function testInvalidExceptionOnChangeState(): void
|
||||
@@ -32,6 +32,12 @@ trait KernelTestTrait
|
||||
return $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TEntity
|
||||
* @param TestFixture<TEntity> $fixture
|
||||
* @return non-empty-array<TEntity>
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function importFixture(TestFixture $fixture): array
|
||||
{
|
||||
return $fixture->load($this->getEntityManager());
|
||||
|
||||
45
tests/Mocks/InvoiceTestMetaFieldSubscriberMock.php
Normal file
45
tests/Mocks/InvoiceTestMetaFieldSubscriberMock.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
|
||||
use App\Entity\InvoiceMeta;
|
||||
use App\Event\InvoiceMetaDefinitionEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
class InvoiceTestMetaFieldSubscriberMock implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
InvoiceMetaDefinitionEvent::class => ['loadMeta', 200],
|
||||
];
|
||||
}
|
||||
|
||||
public function loadMeta(InvoiceMetaDefinitionEvent $event): void
|
||||
{
|
||||
$definition = (new InvoiceMeta())
|
||||
->setName('metatestmock')
|
||||
->setType(TextType::class)
|
||||
->addConstraint(new Length(['max' => 200]))
|
||||
->setIsVisible(true);
|
||||
|
||||
$event->getEntity()->setMetaField($definition);
|
||||
|
||||
$definition = (new InvoiceMeta())
|
||||
->setName('foobar')
|
||||
->setType(IntegerType::class)
|
||||
->setIsVisible(false);
|
||||
|
||||
$event->getEntity()->setMetaField($definition);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
namespace App\Tests\Security;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Repository\RolePermissionRepository;
|
||||
use App\Security\RolePermissionManager;
|
||||
@@ -115,4 +119,167 @@ class RolePermissionManagerTest extends TestCase
|
||||
self::assertTrue($sut->hasPermission('ROLE_SUPER_ADMIN', 'role_permissions'));
|
||||
self::assertTrue($sut->hasPermission('ROLE_SUPER_ADMIN', 'view_user'));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessCustomerAllowsUsersWithGlobalAccess(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customer->addTeam(new Team('Support'));
|
||||
|
||||
$user = new User();
|
||||
self::assertFalse($sut->checkTeamAccessCustomer($customer, $user));
|
||||
$user->initCanSeeAllData(true);
|
||||
self::assertTrue($sut->checkTeamAccessCustomer($customer, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessCustomerAllowsAccessWithoutAssignedTeams(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
|
||||
self::assertTrue($sut->checkTeamAccessCustomer(new Customer('Acme'), new User()));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessCustomerRequiresMembershipForAssignedTeams(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$team = new Team('Support');
|
||||
$customer->addTeam($team);
|
||||
|
||||
$user = new User();
|
||||
self::assertFalse($sut->checkTeamAccessCustomer($customer, new User()));
|
||||
|
||||
self::assertFalse($sut->checkTeamAccessCustomer($customer, $user));
|
||||
$team->addUser($user);
|
||||
self::assertTrue($sut->checkTeamAccessCustomer($customer, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessProjectDeniesAccessIfCustomerIsDenied(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customerTeam = new Team('Customer team');
|
||||
$customer->addTeam($customerTeam);
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$projectTeam = new Team('Project team');
|
||||
$project->addTeam($projectTeam);
|
||||
|
||||
$user = new User();
|
||||
$projectTeam->addUser($user);
|
||||
|
||||
self::assertFalse($sut->checkTeamAccessProject($project, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessProjectAllowsUsersWithGlobalAccess(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customer->addTeam(new Team('Customer team'));
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$project->addTeam(new Team('Project team'));
|
||||
|
||||
$user = new User();
|
||||
self::assertFalse($sut->checkTeamAccessProject($project, $user));
|
||||
$user->initCanSeeAllData(true);
|
||||
self::assertTrue($sut->checkTeamAccessProject($project, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessProjectAllowsMatchingProjectTeamAfterCustomerAccess(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customerTeam = new Team('Customer team');
|
||||
$customer->addTeam($customerTeam);
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$projectTeam = new Team('Project team');
|
||||
$project->addTeam($projectTeam);
|
||||
|
||||
$user = new User();
|
||||
$customerTeam->addUser($user);
|
||||
$projectTeam->addUser($user);
|
||||
|
||||
self::assertTrue($sut->checkTeamAccessProject($project, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessActivityDeniesAccessIfProjectIsDenied(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customerTeam = new Team('Customer team');
|
||||
$customer->addTeam($customerTeam);
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$projectTeam = new Team('Project team');
|
||||
$project->addTeam($projectTeam);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setProject($project);
|
||||
$activityTeam = new Team('Activity team');
|
||||
$activity->addTeam($activityTeam);
|
||||
|
||||
$user = new User();
|
||||
$activityTeam->addUser($user);
|
||||
|
||||
self::assertFalse($sut->checkTeamAccessActivity($activity, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessActivityAllowsUsersWithGlobalAccess(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customer->addTeam(new Team('Customer team'));
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$project->addTeam(new Team('Project team'));
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setProject($project);
|
||||
$activity->addTeam(new Team('Activity team'));
|
||||
|
||||
$user = new User();
|
||||
self::assertFalse($sut->checkTeamAccessActivity($activity, $user));
|
||||
$user->initCanSeeAllData(true);
|
||||
self::assertTrue($sut->checkTeamAccessActivity($activity, $user));
|
||||
}
|
||||
|
||||
public function testCheckTeamAccessActivityAllowsMatchingActivityTeamAfterProjectAccess(): void
|
||||
{
|
||||
$sut = $this->createSut();
|
||||
$customer = new Customer('Acme');
|
||||
$customerTeam = new Team('Customer team');
|
||||
$customer->addTeam($customerTeam);
|
||||
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
$projectTeam = new Team('Project team');
|
||||
$project->addTeam($projectTeam);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setProject($project);
|
||||
$activityTeam = new Team('Activity team');
|
||||
$activity->addTeam($activityTeam);
|
||||
|
||||
$user = new User();
|
||||
$customerTeam->addUser($user);
|
||||
$projectTeam->addUser($user);
|
||||
$activityTeam->addUser($user);
|
||||
|
||||
self::assertTrue($sut->checkTeamAccessActivity($activity, $user));
|
||||
}
|
||||
|
||||
private function createSut(): RolePermissionManager
|
||||
{
|
||||
$repository = $this->getMockBuilder(RolePermissionRepository::class)->onlyMethods(['getAllAsArray'])->disableOriginalConstructor()->getMock();
|
||||
$repository->method('getAllAsArray')->willReturn([]);
|
||||
|
||||
return new RolePermissionManager(new PermissionService($repository, new ArrayAdapter()), [], []);
|
||||
}
|
||||
}
|
||||
|
||||
134
tests/Voter/InvoiceVoterTest.php
Normal file
134
tests/Voter/InvoiceVoterTest.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?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\Voter;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Invoice;
|
||||
use App\Entity\Team;
|
||||
use App\Entity\User;
|
||||
use App\Voter\InvoiceVoter;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
|
||||
#[CoversClass(InvoiceVoter::class)]
|
||||
class InvoiceVoterTest extends AbstractVoterTestCase
|
||||
{
|
||||
#[DataProvider('getVoteData')]
|
||||
public function testVote(User $user, mixed $subject, string $attribute, int $result): void
|
||||
{
|
||||
$this->assertVote($user, $subject, $attribute, $result);
|
||||
}
|
||||
|
||||
public function testVoteDeniesIfTokenHasNoApplicationUser(): void
|
||||
{
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token->method('getUser')->willReturn(null);
|
||||
$sut = $this->getVoter(InvoiceVoter::class);
|
||||
|
||||
self::assertEquals(VoterInterface::ACCESS_DENIED, $sut->vote($token, $this->createInvoice(), ['view_invoice']));
|
||||
}
|
||||
|
||||
public function testVoteDeniesIfInvoiceHasNoCustomer(): void
|
||||
{
|
||||
$this->assertVote(self::getUser(2, User::ROLE_TEAMLEAD), new Invoice(), 'view_invoice', VoterInterface::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
public function testVoteRequiresCustomerTeamAccess(): void
|
||||
{
|
||||
$customer = new Customer('Acme');
|
||||
$customer->addTeam(new Team('Accounting'));
|
||||
$invoice = $this->createInvoice($customer);
|
||||
|
||||
$this->assertVote(self::getUser(2, User::ROLE_TEAMLEAD), $invoice, 'view_invoice', VoterInterface::ACCESS_DENIED);
|
||||
|
||||
$team = new Team('Accounting');
|
||||
$user = new User();
|
||||
$user->addRole(User::ROLE_TEAMLEAD);
|
||||
$team->addTeamlead($user);
|
||||
|
||||
$customer = new Customer('Acme');
|
||||
$customer->addTeam($team);
|
||||
$invoice = $this->createInvoice($customer);
|
||||
|
||||
$this->assertVote($user, $invoice, 'view_invoice', VoterInterface::ACCESS_GRANTED);
|
||||
$this->assertVote($user, $invoice, 'edit_invoice', VoterInterface::ACCESS_GRANTED);
|
||||
}
|
||||
|
||||
public function testDeleteInvoiceRequiresDeletePermission(): void
|
||||
{
|
||||
$permissions = [
|
||||
'ROLE_TEAMLEAD' => ['view_invoice', 'create_invoice', 'delete_invoice'],
|
||||
];
|
||||
|
||||
$team = new Team('Accounting');
|
||||
$user = new User();
|
||||
$user->addRole(User::ROLE_TEAMLEAD);
|
||||
$team->addTeamlead($user);
|
||||
|
||||
$customer = new Customer('Acme');
|
||||
$customer->addTeam($team);
|
||||
|
||||
$token = new UsernamePasswordToken($user, 'bar', $user->getRoles());
|
||||
$sut = new InvoiceVoter($this->getRolePermissionManager($permissions, true));
|
||||
|
||||
self::assertEquals(VoterInterface::ACCESS_GRANTED, $sut->vote($token, $this->createInvoice($customer), ['delete_invoice']));
|
||||
}
|
||||
|
||||
public static function getVoteData(): \Generator
|
||||
{
|
||||
$invoice = self::createStaticInvoice();
|
||||
|
||||
yield [self::getUser(0, 'foo'), $invoice, 'view_invoice', VoterInterface::ACCESS_DENIED];
|
||||
yield [self::getUser(1, User::ROLE_USER), $invoice, 'view_invoice', VoterInterface::ACCESS_DENIED];
|
||||
yield [self::getUser(2, User::ROLE_TEAMLEAD), $invoice, 'view_invoice', VoterInterface::ACCESS_GRANTED];
|
||||
yield [self::getUser(2, User::ROLE_TEAMLEAD), $invoice, 'edit_invoice', VoterInterface::ACCESS_GRANTED];
|
||||
yield [self::getUser(2, User::ROLE_TEAMLEAD), $invoice, 'delete_invoice', VoterInterface::ACCESS_DENIED];
|
||||
yield [self::getUser(3, User::ROLE_ADMIN), $invoice, 'view_invoice', VoterInterface::ACCESS_GRANTED];
|
||||
yield [self::getUser(3, User::ROLE_ADMIN), $invoice, 'edit_invoice', VoterInterface::ACCESS_GRANTED];
|
||||
yield [self::getUser(3, User::ROLE_ADMIN), $invoice, 'delete_invoice', VoterInterface::ACCESS_DENIED];
|
||||
yield [self::getUser(4, User::ROLE_SUPER_ADMIN), $invoice, 'view_invoice', VoterInterface::ACCESS_GRANTED];
|
||||
yield [self::getUser(4, User::ROLE_SUPER_ADMIN), $invoice, 'edit_invoice', VoterInterface::ACCESS_GRANTED];
|
||||
yield [self::getUser(4, User::ROLE_SUPER_ADMIN), $invoice, 'delete_invoice', VoterInterface::ACCESS_DENIED];
|
||||
|
||||
$result = VoterInterface::ACCESS_ABSTAIN;
|
||||
yield [self::getUser(2, User::ROLE_TEAMLEAD), $invoice, 'view', $result];
|
||||
yield [self::getUser(2, User::ROLE_TEAMLEAD), new \stdClass(), 'view_invoice', $result];
|
||||
yield [self::getUser(2, User::ROLE_TEAMLEAD), null, 'edit_invoice', $result];
|
||||
}
|
||||
|
||||
private function assertVote(User $user, mixed $subject, string $attribute, int $result): void
|
||||
{
|
||||
$token = new UsernamePasswordToken($user, 'bar', $user->getRoles());
|
||||
$sut = $this->getVoter(InvoiceVoter::class);
|
||||
|
||||
self::assertEquals($result, $sut->vote($token, $subject, [$attribute]));
|
||||
}
|
||||
|
||||
private function createInvoice(?Customer $customer = null): Invoice
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
if ($customer !== null) {
|
||||
$invoice->setCustomer($customer);
|
||||
}
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
private static function createStaticInvoice(): Invoice
|
||||
{
|
||||
$invoice = new Invoice();
|
||||
$invoice->setCustomer(new Customer('Acme'));
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
}
|
||||
@@ -356,11 +356,6 @@ parameters:
|
||||
count: 1
|
||||
path: Command/ExportCreateCommandTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Command\\\\ExportCreateCommandTest\\:\\:importFixture\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Command/ExportCreateCommandTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Command\\\\InvoiceCreateCommandTest\\:\\:assertCommandErrors\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -371,11 +366,6 @@ parameters:
|
||||
count: 1
|
||||
path: Command/InvoiceCreateCommandTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Command\\\\InvoiceCreateCommandTest\\:\\:importFixture\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Command/InvoiceCreateCommandTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Command\\\\PluginCommandTest\\:\\:getCommandTester\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -526,11 +516,6 @@ parameters:
|
||||
count: 1
|
||||
path: Controller/AbstractControllerBaseTestCase.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Controller\\\\AbstractControllerBaseTestCase\\:\\:importFixture\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Controller/AbstractControllerBaseTestCase.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Controller\\\\AbstractControllerBaseTestCase\\:\\:request\\(\\) has parameter \\$parameters with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -991,11 +976,6 @@ parameters:
|
||||
count: 3
|
||||
path: Controller/WidgetControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\DataFixtures\\\\TestFixture\\:\\:load\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: DataFixtures/TestFixture.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 'foo\\-bundle\\.bar' on array\\|bool\\|float\\|int\\|string\\|null\\.$#"
|
||||
count: 1
|
||||
@@ -1569,17 +1549,17 @@ parameters:
|
||||
-
|
||||
message: "#^Cannot call method getTotal\\(\\) on App\\\\Invoice\\\\CalculatorInterface\\|null\\.$#"
|
||||
count: 4
|
||||
path: Invoice/ServiceInvoiceTest.php
|
||||
path: Invoice/InvoiceServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\ServiceInvoiceTest\\:\\:getSut\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#"
|
||||
message: "#^Method App\\\\Tests\\\\Invoice\\\\InvoiceServiceTest\\:\\:getSut\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Invoice/ServiceInvoiceTest.php
|
||||
path: Invoice/InvoiceServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$dataDir of class App\\\\Utils\\\\FileHelper constructor expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
path: Invoice/ServiceInvoiceTest.php
|
||||
path: Invoice/InvoiceServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Ldap\\\\LdapManagerTest\\:\\:getLdapManager\\(\\) has no return type specified\\.$#"
|
||||
@@ -1701,11 +1681,6 @@ parameters:
|
||||
count: 1
|
||||
path: Repository/AbstractRepositoryTestCase.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Repository\\\\AbstractRepositoryTestCase\\:\\:importFixture\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Repository/AbstractRepositoryTestCase.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$directory of method App\\\\Repository\\\\InvoiceDocumentRepository\\:\\:addDirectory\\(\\) expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
|
||||
Reference in New Issue
Block a user