mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-12 14:31:35 +00:00
Merge branch 'improve_test_coverage'
This commit is contained in:
commit
a6ef9a58ec
31 changed files with 4682 additions and 1769 deletions
436
composer.lock
generated
436
composer.lock
generated
File diff suppressed because it is too large
Load diff
2882
config/reference.php
2882
config/reference.php
File diff suppressed because it is too large
Load diff
222
tests/Controller/AuthorizationTest.php
Normal file
222
tests/Controller/AuthorizationTest.php
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\UserSystem\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the HTTP access-control boundaries:
|
||||||
|
*
|
||||||
|
* The app has an "anonymous" fixture user with readonly permissions, so truly
|
||||||
|
* public read routes return 200 even without a session. Write-protected routes
|
||||||
|
* return 401 for unauthenticated requests (not a 302 redirect).
|
||||||
|
*
|
||||||
|
* Users: admin (all-allow), user (editor preset), noread (no group/no perms)
|
||||||
|
*/
|
||||||
|
#[Group('DB')]
|
||||||
|
#[Group('slow')]
|
||||||
|
final class AuthorizationTest extends WebTestCase
|
||||||
|
{
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Data providers
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes readable by the anonymous user — unauthenticated requests get 200.
|
||||||
|
*/
|
||||||
|
public static function publicReadRoutesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'homepage' => ['/en/'];
|
||||||
|
yield 'part view' => ['/en/part/1'];
|
||||||
|
yield 'statistics' => ['/en/statistics'];
|
||||||
|
yield 'select category' => ['/en/select_api/category'];
|
||||||
|
yield 'typeahead tags' => ['/en/typeahead/tags/search/test'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write-protected routes — unauthenticated gets 401 (not 302).
|
||||||
|
*/
|
||||||
|
public static function writeProtectedRoutesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'part edit' => ['/en/part/1/edit'];
|
||||||
|
yield 'part new' => ['/en/part/new'];
|
||||||
|
yield 'user edit' => ['/en/user/1/edit'];
|
||||||
|
yield 'log list' => ['/en/log/'];
|
||||||
|
yield 'server info' => ['/en/tools/server_infos'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes the `noread` user (no group = no permissions) must be denied.
|
||||||
|
*/
|
||||||
|
public static function noreadDeniedRoutesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'part view' => ['/en/part/1'];
|
||||||
|
yield 'part edit' => ['/en/part/1/edit'];
|
||||||
|
yield 'part new' => ['/en/part/new'];
|
||||||
|
yield 'log list' => ['/en/log/'];
|
||||||
|
yield 'server info' => ['/en/tools/server_infos'];
|
||||||
|
yield 'select category' => ['/en/select_api/category'];
|
||||||
|
yield 'typeahead tags' => ['/en/typeahead/tags/search/test'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes the `user` (editor preset) must have access to.
|
||||||
|
*/
|
||||||
|
public static function editorAllowedRoutesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'homepage' => ['/en/'];
|
||||||
|
yield 'part view' => ['/en/part/1'];
|
||||||
|
yield 'part edit' => ['/en/part/1/edit'];
|
||||||
|
yield 'part new' => ['/en/part/new'];
|
||||||
|
yield 'select cat' => ['/en/select_api/category'];
|
||||||
|
yield 'typeahead' => ['/en/typeahead/tags/search/test'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin-only routes the `user` (editor) must be denied.
|
||||||
|
*/
|
||||||
|
public static function editorDeniedRoutesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'user edit' => ['/en/user/1/edit'];
|
||||||
|
yield 'log list' => ['/en/log/'];
|
||||||
|
yield 'server info' => ['/en/tools/server_infos'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes the `admin` user must be able to reach.
|
||||||
|
*/
|
||||||
|
public static function adminAllowedRoutesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'user edit' => ['/en/user/1/edit'];
|
||||||
|
yield 'log list' => ['/en/log/'];
|
||||||
|
yield 'server info' => ['/en/tools/server_infos'];
|
||||||
|
yield 'part view' => ['/en/part/1'];
|
||||||
|
yield 'part edit' => ['/en/part/1/edit'];
|
||||||
|
yield 'statistics' => ['/en/statistics'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
private function loginAs(string $username): KernelBrowser
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$user = $em->getRepository(User::class)->findOneBy(['name' => $username]);
|
||||||
|
if ($user === null) {
|
||||||
|
$this->markTestSkipped("Fixture user '$username' not found.");
|
||||||
|
}
|
||||||
|
$client->loginUser($user);
|
||||||
|
$client->followRedirects(false);
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertDenied(KernelBrowser $client, string $url): void
|
||||||
|
{
|
||||||
|
$client->request('GET', $url);
|
||||||
|
$code = $client->getResponse()->getStatusCode();
|
||||||
|
$this->assertTrue(
|
||||||
|
$code === Response::HTTP_FORBIDDEN || $code === Response::HTTP_UNAUTHORIZED || $client->getResponse()->isRedirect(),
|
||||||
|
"Expected 401/403/redirect on $url, got $code"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Unauthenticated: public reads
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('publicReadRoutesProvider')]
|
||||||
|
public function testUnauthenticatedCanReadPublicRoutes(string $url): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('GET', $url);
|
||||||
|
// Anonymous user (readonly group) can access read-only content
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Unauthenticated: write routes → 401
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('writeProtectedRoutesProvider')]
|
||||||
|
public function testUnauthenticatedIsUnauthorizedOnWriteRoutes(string $url): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->followRedirects(false);
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$code = $client->getResponse()->getStatusCode();
|
||||||
|
$this->assertTrue(
|
||||||
|
$code === Response::HTTP_UNAUTHORIZED || $client->getResponse()->isRedirect(),
|
||||||
|
"Expected 401 or redirect on $url for unauthenticated request, got $code"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// noread user: denied everywhere
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('noreadDeniedRoutesProvider')]
|
||||||
|
public function testNoreadUserIsDenied(string $url): void
|
||||||
|
{
|
||||||
|
$this->assertDenied($this->loginAs('noread'), $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Editor user
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('editorAllowedRoutesProvider')]
|
||||||
|
public function testEditorCanAccess(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->loginAs('user');
|
||||||
|
$client->request('GET', $url);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('editorDeniedRoutesProvider')]
|
||||||
|
public function testEditorIsDeniedOnAdminRoutes(string $url): void
|
||||||
|
{
|
||||||
|
$this->assertDenied($this->loginAs('user'), $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Admin user: can access everything
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('adminAllowedRoutesProvider')]
|
||||||
|
public function testAdminCanAccessAllRoutes(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->loginAs('admin');
|
||||||
|
$client->request('GET', $url);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
152
tests/Controller/SelectApiControllerTest.php
Normal file
152
tests/Controller/SelectApiControllerTest.php
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\UserSystem\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the SelectAPIController endpoints used by select2 widgets.
|
||||||
|
* These JSON endpoints back every structural-entity dropdown in the UI.
|
||||||
|
*/
|
||||||
|
#[Group('DB')]
|
||||||
|
#[Group('slow')]
|
||||||
|
final class SelectApiControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public static function endpointProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'category' => ['/en/select_api/category'];
|
||||||
|
yield 'footprint' => ['/en/select_api/footprint'];
|
||||||
|
yield 'manufacturer' => ['/en/select_api/manufacturer'];
|
||||||
|
yield 'measurement_unit' => ['/en/select_api/measurement_unit'];
|
||||||
|
yield 'project' => ['/en/select_api/project'];
|
||||||
|
yield 'storage_location' => ['/en/select_api/storage_location'];
|
||||||
|
yield 'label_profiles' => ['/en/select_api/label_profiles'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function adminClient(): KernelBrowser
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$admin = $em->getRepository(User::class)->findOneBy(['name' => 'admin']);
|
||||||
|
if ($admin === null) {
|
||||||
|
$this->markTestSkipped('Fixture user admin not found.');
|
||||||
|
}
|
||||||
|
$client->loginUser($admin);
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Response format
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEndpointReturns200WithJsonContentType(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->adminClient();
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertResponseHeaderSame('content-type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEndpointReturnsValidJsonArray(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->adminClient();
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$body = $client->getResponse()->getContent();
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
|
||||||
|
$this->assertIsArray($decoded, "Response from $url is not a valid JSON array");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEachEntryHasTextAndValueKeys(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->adminClient();
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$decoded = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
// Some endpoints include an empty "select none" entry at index 0; all entries must have text + value
|
||||||
|
foreach ($decoded as $entry) {
|
||||||
|
$this->assertArrayHasKey('text', $entry, "Entry in $url missing 'text' key");
|
||||||
|
$this->assertArrayHasKey('value', $entry, "Entry in $url missing 'value' key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Access control
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testUnauthenticatedCanReadSelectApi(string $url): void
|
||||||
|
{
|
||||||
|
// The anonymous user (readonly group) has read access to structural entities,
|
||||||
|
// so these endpoints return 200 even without a session.
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('GET', $url);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testNoreadUserIsDenied(string $url): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$noread = $em->getRepository(User::class)->findOneBy(['name' => 'noread']);
|
||||||
|
if ($noread === null) {
|
||||||
|
$this->markTestSkipped('Fixture user noread not found.');
|
||||||
|
}
|
||||||
|
$client->loginUser($noread);
|
||||||
|
$client->followRedirects(false);
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->getStatusCode() === 403 || $response->isRedirect(),
|
||||||
|
"Expected 403 or redirect for noread user on $url, got " . $response->getStatusCode()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEditorUserCanAccess(string $url): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$user = $em->getRepository(User::class)->findOneBy(['name' => 'user']);
|
||||||
|
if ($user === null) {
|
||||||
|
$this->markTestSkipped('Fixture user user not found.');
|
||||||
|
}
|
||||||
|
$client->loginUser($user);
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
162
tests/Controller/TypeaheadControllerTest.php
Normal file
162
tests/Controller/TypeaheadControllerTest.php
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\UserSystem\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the TypeaheadController JSON endpoints that back autocomplete widgets in the UI.
|
||||||
|
*/
|
||||||
|
#[Group('DB')]
|
||||||
|
#[Group('slow')]
|
||||||
|
final class TypeaheadControllerTest extends WebTestCase
|
||||||
|
{
|
||||||
|
public static function endpointProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'tags search' => ['/en/typeahead/tags/search/test'];
|
||||||
|
yield 'parameters part search' => ['/en/typeahead/parameters/part/search/voltage'];
|
||||||
|
yield 'parameters category search' => ['/en/typeahead/parameters/category/search/NPN'];
|
||||||
|
yield 'builtin resources' => ['/en/typeahead/builtInResources/search?query=DIP'];
|
||||||
|
yield 'parts search' => ['/en/typeahead/parts/search/res'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function partsReadEndpointProvider(): \Generator
|
||||||
|
{
|
||||||
|
// These require @parts.read — noread user must be denied
|
||||||
|
yield 'tags search' => ['/en/typeahead/tags/search/test'];
|
||||||
|
yield 'parameters part search' => ['/en/typeahead/parameters/part/search/voltage'];
|
||||||
|
yield 'parts search' => ['/en/typeahead/parts/search/res'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loginClient(string $username): KernelBrowser
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$user = $em->getRepository(User::class)->findOneBy(['name' => $username]);
|
||||||
|
if ($user === null) {
|
||||||
|
$this->markTestSkipped("Fixture user '$username' not found.");
|
||||||
|
}
|
||||||
|
$client->loginUser($user);
|
||||||
|
return $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Response format
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEndpointReturnsSuccessfulJsonForAdmin(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->loginClient('admin');
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
$this->assertJson($client->getResponse()->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEndpointReturnsJsonArray(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->loginClient('admin');
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$decoded = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertIsArray($decoded, "Response from $url should be a JSON array");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Tags search: result structure
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function testTagsSearchReturnsStrings(): void
|
||||||
|
{
|
||||||
|
$client = $this->loginClient('admin');
|
||||||
|
$client->request('GET', '/en/typeahead/tags/search/a');
|
||||||
|
|
||||||
|
$tags = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertIsArray($tags);
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$this->assertIsString($tag, 'Each tag entry should be a plain string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Parts search: result structure
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function testPartsSearchReturnsArrayWithExpectedKeys(): void
|
||||||
|
{
|
||||||
|
$client = $this->loginClient('admin');
|
||||||
|
$client->request('GET', '/en/typeahead/parts/search/test');
|
||||||
|
|
||||||
|
$parts = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertIsArray($parts);
|
||||||
|
// Each result must have at least id and name
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->assertArrayHasKey('id', $part);
|
||||||
|
$this->assertArrayHasKey('name', $part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Access control
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testUnauthenticatedCanAccessTypeahead(string $url): void
|
||||||
|
{
|
||||||
|
// Anonymous user (readonly group) has @parts.read, so these endpoints return 200.
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('GET', $url);
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('partsReadEndpointProvider')]
|
||||||
|
public function testNoreadUserIsDenied(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->loginClient('noread');
|
||||||
|
$client->followRedirects(false);
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$this->assertTrue(
|
||||||
|
$response->getStatusCode() === 403 || $response->isRedirect(),
|
||||||
|
"Expected 403 or redirect for noread user on $url, got " . $response->getStatusCode()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('endpointProvider')]
|
||||||
|
public function testEditorUserCanAccess(string $url): void
|
||||||
|
{
|
||||||
|
$client = $this->loginClient('user');
|
||||||
|
$client->request('GET', $url);
|
||||||
|
|
||||||
|
$this->assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
103
tests/EventSubscriber/MaintenanceModeSubscriberTest.php
Normal file
103
tests/EventSubscriber/MaintenanceModeSubscriberTest.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\EventSubscriber;
|
||||||
|
|
||||||
|
use App\EventSubscriber\MaintenanceModeSubscriber;
|
||||||
|
use App\Services\System\UpdateExecutor;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
|
|
||||||
|
final class MaintenanceModeSubscriberTest extends TestCase
|
||||||
|
{
|
||||||
|
private function makeSubscriber(bool $maintenanceActive): MaintenanceModeSubscriber
|
||||||
|
{
|
||||||
|
$executor = $this->createMock(UpdateExecutor::class);
|
||||||
|
$executor->method('isMaintenanceMode')->willReturn($maintenanceActive);
|
||||||
|
$executor->method('getMaintenanceInfo')->willReturn(
|
||||||
|
$maintenanceActive ? ['reason' => 'Test update', 'enabled_at' => date('Y-m-d H:i:s')] : null
|
||||||
|
);
|
||||||
|
return new MaintenanceModeSubscriber($executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeEvent(string $url = 'http://example.com/'): RequestEvent
|
||||||
|
{
|
||||||
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
|
$request = Request::create($url);
|
||||||
|
return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoMaintenanceModeDoesNotSetResponse(): void
|
||||||
|
{
|
||||||
|
$subscriber = $this->makeSubscriber(false);
|
||||||
|
$event = $this->makeEvent();
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
// When not in maintenance mode, no response is ever set regardless of SAPI
|
||||||
|
$this->assertFalse($event->hasResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCliRequestIsNeverBlocked(): void
|
||||||
|
{
|
||||||
|
// Tests run from CLI (PHP_SAPI === 'cli'), so maintenance mode never blocks CLI requests.
|
||||||
|
// This verifies the intentional behaviour: maintenance mode only affects web requests.
|
||||||
|
$subscriber = $this->makeSubscriber(true);
|
||||||
|
$event = $this->makeEvent();
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
// CLI requests pass through even with maintenance active
|
||||||
|
$this->assertFalse($event->hasResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubRequestIsIgnored(): void
|
||||||
|
{
|
||||||
|
$subscriber = $this->makeSubscriber(true);
|
||||||
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
|
$request = Request::create('http://example.com/');
|
||||||
|
$event = new RequestEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST);
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
$this->assertFalse($event->hasResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscriberListensToKernelRequest(): void
|
||||||
|
{
|
||||||
|
$events = MaintenanceModeSubscriber::getSubscribedEvents();
|
||||||
|
$this->assertArrayHasKey(KernelEvents::REQUEST, $events);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscriberListensWithHighPriority(): void
|
||||||
|
{
|
||||||
|
$events = MaintenanceModeSubscriber::getSubscribedEvents();
|
||||||
|
$config = $events[KernelEvents::REQUEST];
|
||||||
|
// Config is ['methodName', priority]
|
||||||
|
$priority = is_array($config) ? (int) ($config[1] ?? 0) : 0;
|
||||||
|
$this->assertGreaterThan(0, $priority, 'Maintenance subscriber should run with high priority');
|
||||||
|
}
|
||||||
|
}
|
||||||
101
tests/EventSubscriber/RedirectToHttpsSubscriberTest.php
Normal file
101
tests/EventSubscriber/RedirectToHttpsSubscriberTest.php
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\EventSubscriber;
|
||||||
|
|
||||||
|
use App\EventSubscriber\RedirectToHttpsSubscriber;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||||
|
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||||
|
use Symfony\Component\Security\Http\HttpUtils;
|
||||||
|
|
||||||
|
final class RedirectToHttpsSubscriberTest extends TestCase
|
||||||
|
{
|
||||||
|
private function makeEvent(string $url, bool $isMainRequest = true): RequestEvent
|
||||||
|
{
|
||||||
|
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||||
|
$request = Request::create($url);
|
||||||
|
return new RequestEvent($kernel, $request, $isMainRequest ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::SUB_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpRequestIsRedirectedToHttpsWhenEnabled(): void
|
||||||
|
{
|
||||||
|
$subscriber = new RedirectToHttpsSubscriber(true, new HttpUtils());
|
||||||
|
$event = $this->makeEvent('http://example.com/some/path');
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
$this->assertTrue($event->hasResponse());
|
||||||
|
$response = $event->getResponse();
|
||||||
|
$this->assertStringStartsWith('https://', $response->getTargetUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpsRequestIsNotRedirectedWhenEnabled(): void
|
||||||
|
{
|
||||||
|
$subscriber = new RedirectToHttpsSubscriber(true, new HttpUtils());
|
||||||
|
$event = $this->makeEvent('https://example.com/some/path');
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
$this->assertFalse($event->hasResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpRequestIsNotRedirectedWhenDisabled(): void
|
||||||
|
{
|
||||||
|
$subscriber = new RedirectToHttpsSubscriber(false, new HttpUtils());
|
||||||
|
$event = $this->makeEvent('http://example.com/some/path');
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
$this->assertFalse($event->hasResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubRequestIsNotRedirected(): void
|
||||||
|
{
|
||||||
|
$subscriber = new RedirectToHttpsSubscriber(true, new HttpUtils());
|
||||||
|
$event = $this->makeEvent('http://example.com/', false);
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
$this->assertFalse($event->hasResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRedirectUrlPreservesPath(): void
|
||||||
|
{
|
||||||
|
$subscriber = new RedirectToHttpsSubscriber(true, new HttpUtils());
|
||||||
|
$event = $this->makeEvent('http://example.com/admin/parts?q=test');
|
||||||
|
|
||||||
|
$subscriber->onKernelRequest($event);
|
||||||
|
|
||||||
|
$this->assertTrue($event->hasResponse());
|
||||||
|
$this->assertStringContainsString('/admin/parts', $event->getResponse()->getTargetUrl());
|
||||||
|
$this->assertStringContainsString('q=test', $event->getResponse()->getTargetUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSubscriberListensToKernelRequestEvent(): void
|
||||||
|
{
|
||||||
|
$events = RedirectToHttpsSubscriber::getSubscribedEvents();
|
||||||
|
$this->assertArrayHasKey('kernel.request', $events);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
tests/Services/Cache/ElementCacheTagGeneratorTest.php
Normal file
67
tests/Services/Cache/ElementCacheTagGeneratorTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\Cache;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Services\Cache\ElementCacheTagGenerator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ElementCacheTagGeneratorTest extends TestCase
|
||||||
|
{
|
||||||
|
private ElementCacheTagGenerator $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->service = new ElementCacheTagGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClassNameIsConvertedToTag(): void
|
||||||
|
{
|
||||||
|
$tag = $this->service->getElementTypeCacheTag(Part::class);
|
||||||
|
// Backslashes must be replaced by underscores
|
||||||
|
$this->assertStringNotContainsString('\\', $tag);
|
||||||
|
$this->assertSame(str_replace('\\', '_', Part::class), $tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testObjectInputGivesSameResultAsClassName(): void
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
$tagFromObject = $this->service->getElementTypeCacheTag($part);
|
||||||
|
$tagFromClass = $this->service->getElementTypeCacheTag(Part::class);
|
||||||
|
$this->assertSame($tagFromClass, $tagFromObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResultIsCached(): void
|
||||||
|
{
|
||||||
|
$tag1 = $this->service->getElementTypeCacheTag(Part::class);
|
||||||
|
$tag2 = $this->service->getElementTypeCacheTag(Part::class);
|
||||||
|
$this->assertSame($tag1, $tag2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNonExistentClassThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->getElementTypeCacheTag('App\\NonExistent\\Foo');
|
||||||
|
}
|
||||||
|
}
|
||||||
110
tests/Services/Cache/UserCacheKeyGeneratorTest.php
Normal file
110
tests/Services/Cache/UserCacheKeyGeneratorTest.php
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\Cache;
|
||||||
|
|
||||||
|
use App\Entity\UserSystem\User;
|
||||||
|
use App\Services\Cache\UserCacheKeyGenerator;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
final class UserCacheKeyGeneratorTest extends TestCase
|
||||||
|
{
|
||||||
|
private function makeGenerator(?User $loggedInUser, ?Request $request = null): UserCacheKeyGenerator
|
||||||
|
{
|
||||||
|
$security = $this->createMock(Security::class);
|
||||||
|
$security->method('getUser')->willReturn($loggedInUser);
|
||||||
|
|
||||||
|
$requestStack = $this->createMock(RequestStack::class);
|
||||||
|
$requestStack->method('getCurrentRequest')->willReturn($request);
|
||||||
|
|
||||||
|
return new UserCacheKeyGenerator($security, $requestStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeUserWithId(int $id): User
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$ref = new \ReflectionProperty(User::class, 'id');
|
||||||
|
$ref->setValue($user, $id);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnonymousUserKeyContainsAnonymousId(): void
|
||||||
|
{
|
||||||
|
$service = $this->makeGenerator(null);
|
||||||
|
$key = $service->generateKey();
|
||||||
|
$this->assertStringContainsString((string) User::ID_ANONYMOUS, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExplicitAnonymousUserGivesSameKeyAsNull(): void
|
||||||
|
{
|
||||||
|
$anonUser = $this->makeUserWithId(User::ID_ANONYMOUS);
|
||||||
|
$anonUser->setName('anonymous');
|
||||||
|
|
||||||
|
$service = $this->makeGenerator(null);
|
||||||
|
$keyFromNull = $service->generateKey(null);
|
||||||
|
$keyFromAnon = $service->generateKey($anonUser);
|
||||||
|
$this->assertSame($keyFromNull, $keyFromAnon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKeyForRealUserContainsUserId(): void
|
||||||
|
{
|
||||||
|
$user = $this->makeUserWithId(42);
|
||||||
|
$service = $this->makeGenerator(null);
|
||||||
|
|
||||||
|
$key = $service->generateKey($user);
|
||||||
|
$this->assertStringContainsString('42', $key);
|
||||||
|
$this->assertStringNotContainsString((string) User::ID_ANONYMOUS, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLocaleFromRequestIsIncludedInKey(): void
|
||||||
|
{
|
||||||
|
$request = Request::create('/');
|
||||||
|
$request->setLocale('de');
|
||||||
|
|
||||||
|
$service = $this->makeGenerator(null, $request);
|
||||||
|
$key = $service->generateKey();
|
||||||
|
$this->assertStringContainsString('de', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDifferentUsersProduceDifferentKeys(): void
|
||||||
|
{
|
||||||
|
$service = $this->makeGenerator(null);
|
||||||
|
|
||||||
|
$user1 = $this->makeUserWithId(10);
|
||||||
|
$user2 = $this->makeUserWithId(20);
|
||||||
|
|
||||||
|
$this->assertNotSame($service->generateKey($user1), $service->generateKey($user2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCurrentlyLoggedInUserIsUsedWhenNoExplicitUser(): void
|
||||||
|
{
|
||||||
|
$loggedIn = $this->makeUserWithId(99);
|
||||||
|
$service = $this->makeGenerator($loggedIn);
|
||||||
|
|
||||||
|
$key = $service->generateKey();
|
||||||
|
$this->assertStringContainsString('99', $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
tests/Services/EntityURLGeneratorTest.php
Normal file
113
tests/Services/EntityURLGeneratorTest.php
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services;
|
||||||
|
|
||||||
|
use App\Entity\Base\AbstractDBElement;
|
||||||
|
use App\Entity\Parts\Category;
|
||||||
|
use App\Entity\Parts\Footprint;
|
||||||
|
use App\Entity\Parts\Manufacturer;
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Entity\Parts\StorageLocation;
|
||||||
|
use App\Entity\Parts\Supplier;
|
||||||
|
use App\Entity\UserSystem\User;
|
||||||
|
use App\Exceptions\EntityNotSupportedException;
|
||||||
|
use App\Services\EntityURLGenerator;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
final class EntityURLGeneratorTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static EntityURLGenerator $service;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$service = self::getContainer()->get(EntityURLGenerator::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entityWithId(string $class, int $id): AbstractDBElement
|
||||||
|
{
|
||||||
|
$entity = new $class();
|
||||||
|
$ref = new \ReflectionProperty(AbstractDBElement::class, 'id');
|
||||||
|
$ref->setValue($entity, $id);
|
||||||
|
return $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInfoUrlForPartContainsPartPath(): void
|
||||||
|
{
|
||||||
|
$part = $this->entityWithId(Part::class, 1);
|
||||||
|
$url = self::$service->infoURL($part);
|
||||||
|
$this->assertStringContainsString('part', $url);
|
||||||
|
$this->assertStringContainsString('1', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditUrlForCategoryContainsCategoryPath(): void
|
||||||
|
{
|
||||||
|
$category = $this->entityWithId(Category::class, 5);
|
||||||
|
$url = self::$service->editURL($category);
|
||||||
|
$this->assertStringContainsString('category', $url);
|
||||||
|
$this->assertStringContainsString('5', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListPartsUrlForSupplierContainsSupplierPath(): void
|
||||||
|
{
|
||||||
|
$supplier = $this->entityWithId(Supplier::class, 7);
|
||||||
|
$url = self::$service->listPartsURL($supplier);
|
||||||
|
$this->assertStringContainsString('supplier', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetUrlWithInfoTypeCallsInfoUrl(): void
|
||||||
|
{
|
||||||
|
$part = $this->entityWithId(Part::class, 3);
|
||||||
|
$url = self::$service->getURL($part, 'info');
|
||||||
|
$this->assertStringContainsString('part', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetUrlWithEditTypeCallsEditUrl(): void
|
||||||
|
{
|
||||||
|
$manufacturer = $this->entityWithId(Manufacturer::class, 2);
|
||||||
|
$url = self::$service->getURL($manufacturer, 'edit');
|
||||||
|
$this->assertStringContainsString('manufacturer', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetUrlWithUnknownTypeThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$part = $this->entityWithId(Part::class, 1);
|
||||||
|
self::$service->getURL($part, 'unsupported_type');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInfoUrlForUserContainsUserPath(): void
|
||||||
|
{
|
||||||
|
$user = $this->entityWithId(User::class, 10);
|
||||||
|
$url = self::$service->editURL($user);
|
||||||
|
$this->assertStringContainsString('user', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListPartsUrlForStorelocationContainsStorelocationPath(): void
|
||||||
|
{
|
||||||
|
$loc = $this->entityWithId(StorageLocation::class, 4);
|
||||||
|
$url = self::$service->listPartsURL($loc);
|
||||||
|
$this->assertStringContainsString('store', $url);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
tests/Services/Formatters/MarkdownParserTest.php
Normal file
86
tests/Services/Formatters/MarkdownParserTest.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\Formatters;
|
||||||
|
|
||||||
|
use App\Services\Formatters\MarkdownParser;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
final class MarkdownParserTest extends TestCase
|
||||||
|
{
|
||||||
|
private MarkdownParser $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$translator = $this->createMock(TranslatorInterface::class);
|
||||||
|
$translator->method('trans')->willReturn('Loading...');
|
||||||
|
$this->service = new MarkdownParser($translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOutputContainsDataMarkdownAttribute(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->markForRendering('**hello**');
|
||||||
|
$this->assertStringContainsString('data-markdown=', $result);
|
||||||
|
$this->assertStringContainsString('data-controller="common--markdown"', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMarkdownContentIsHtmlescapedInAttribute(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->markForRendering('<script>alert(1)</script>');
|
||||||
|
// The raw < should not appear unescaped inside the attribute
|
||||||
|
$this->assertStringNotContainsString('<script>', $result);
|
||||||
|
$this->assertStringContainsString('<script>', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInlineModeAddsInlineClass(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->markForRendering('text', true);
|
||||||
|
$this->assertStringContainsString('markdown-inline', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNonInlineModeDoesNotAddInlineClass(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->markForRendering('text', false);
|
||||||
|
$this->assertStringNotContainsString('markdown-inline', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOutputIsWrappedInDiv(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->markForRendering('test');
|
||||||
|
$this->assertStringStartsWith('<div', $result);
|
||||||
|
$this->assertStringEndsWith('</div>', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTranslatorIsCalledForLoadingText(): void
|
||||||
|
{
|
||||||
|
$translator = $this->createMock(TranslatorInterface::class);
|
||||||
|
$translator->expects($this->once())
|
||||||
|
->method('trans')
|
||||||
|
->with('markdown.loading')
|
||||||
|
->willReturn('Loading...');
|
||||||
|
|
||||||
|
$service = new MarkdownParser($translator);
|
||||||
|
$service->markForRendering('test');
|
||||||
|
}
|
||||||
|
}
|
||||||
103
tests/Services/Formatters/MoneyFormatterTest.php
Normal file
103
tests/Services/Formatters/MoneyFormatterTest.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\Formatters;
|
||||||
|
|
||||||
|
use App\Entity\PriceInformations\Currency;
|
||||||
|
use App\Services\Formatters\MoneyFormatter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
final class MoneyFormatterTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static MoneyFormatter $service;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$service = self::getContainer()->get(MoneyFormatter::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatWithFloatInput(): void
|
||||||
|
{
|
||||||
|
$currency = new Currency();
|
||||||
|
$currency->setIsoCode('USD');
|
||||||
|
$result = self::$service->format(1.5, $currency);
|
||||||
|
|
||||||
|
// Output format varies by locale, so verify content not exact form
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertStringContainsString('1', $result);
|
||||||
|
$this->assertTrue(
|
||||||
|
str_contains($result, '$') || str_contains($result, 'USD'),
|
||||||
|
"Expected USD indicator in: $result"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatWithNullCurrencyUsesBaseCurrency(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->format(1.5);
|
||||||
|
// Should return a non-empty formatted string
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertIsString($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatWithExplicitCurrencyUsesThatCurrency(): void
|
||||||
|
{
|
||||||
|
$currency = new Currency();
|
||||||
|
$currency->setIsoCode('USD');
|
||||||
|
|
||||||
|
$result = self::$service->format(10.0, $currency);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertStringContainsString('10', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatStringInputWorksSameAsFloat(): void
|
||||||
|
{
|
||||||
|
$resultFloat = self::$service->format(1.5);
|
||||||
|
$resultString = self::$service->format('1.5');
|
||||||
|
$this->assertSame($resultFloat, $resultString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShowAllDigitsRespectsFractionCount(): void
|
||||||
|
{
|
||||||
|
// With show_all_digits = true and decimals = 3, we expect exactly 3 decimal places
|
||||||
|
$result = self::$service->format(1.5, null, 3, true);
|
||||||
|
// The number should contain exactly 3 decimal digits
|
||||||
|
$this->assertMatchesRegularExpression('/\d{3}(?!\d)/', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroIsFormattedCorrectly(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->format(0.0);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertStringContainsString('0', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCurrencyWithEmptyIsoCodeFallsBackToBaseCurrency(): void
|
||||||
|
{
|
||||||
|
$currency = new Currency();
|
||||||
|
// Empty ISO code → should fall back to base currency
|
||||||
|
$resultWithEmpty = self::$service->format(1.0, $currency);
|
||||||
|
$resultWithNull = self::$service->format(1.0, null);
|
||||||
|
$this->assertSame($resultWithNull, $resultWithEmpty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,4 +87,32 @@ final class EventCommentHelperTest extends WebTestCase
|
||||||
$this->service->clearMessage();
|
$this->service->clearMessage();
|
||||||
$this->assertFalse($this->service->isMessageSet());
|
$this->assertFalse($this->service->isMessageSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEmptyStringTreatedAsNotSet(): void
|
||||||
|
{
|
||||||
|
// Empty string is falsy in PHP, so setMessage('') stores null internally
|
||||||
|
$this->service->setMessage('');
|
||||||
|
$this->assertFalse($this->service->isMessageSet());
|
||||||
|
$this->assertNull($this->service->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMessageNullClearsMessage(): void
|
||||||
|
{
|
||||||
|
$this->service->setMessage('Hello');
|
||||||
|
$this->service->setMessage(null);
|
||||||
|
$this->assertFalse($this->service->isMessageSet());
|
||||||
|
$this->assertNull($this->service->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLongMessageIsTruncated(): void
|
||||||
|
{
|
||||||
|
// MAX_MESSAGE_LENGTH is 255; a longer string should be truncated with '...' suffix
|
||||||
|
$long = str_repeat('a', 300);
|
||||||
|
$this->service->setMessage($long);
|
||||||
|
|
||||||
|
$stored = $this->service->getMessage();
|
||||||
|
$this->assertNotNull($stored);
|
||||||
|
$this->assertLessThanOrEqual(255, mb_strlen($stored));
|
||||||
|
$this->assertStringEndsWith('...', $stored);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
133
tests/Services/LogSystem/LogDataFormatterTest.php
Normal file
133
tests/Services/LogSystem/LogDataFormatterTest.php
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\LogSystem;
|
||||||
|
|
||||||
|
use App\Entity\LogSystem\AbstractLogEntry;
|
||||||
|
use App\Services\LogSystem\LogDataFormatter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
final class LogDataFormatterTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static LogDataFormatter $service;
|
||||||
|
private static AbstractLogEntry $dummyLog;
|
||||||
|
private AbstractLogEntry $dummy;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$service = self::getContainer()->get(LogDataFormatter::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
// A mock is fine: $logEntry is only consulted for @id (foreign key) arrays
|
||||||
|
$this->dummy = $this->createMock(AbstractLogEntry::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringIsWrappedInQuoteSpans(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData('hello', $this->dummy, 'name');
|
||||||
|
$this->assertStringContainsString('"', $result);
|
||||||
|
$this->assertStringContainsString('hello', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringSpecialCharsAreEscaped(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData('<script>', $this->dummy, 'name');
|
||||||
|
$this->assertStringNotContainsString('<script>', $result);
|
||||||
|
$this->assertStringContainsString('<script>', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNewlineInStringRendersAsSpan(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData("line1\nline2", $this->dummy, 'name');
|
||||||
|
$this->assertStringContainsString('\\n', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBoolTrueFormatsAsString(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData(true, $this->dummy, 'enabled');
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBoolFalseFormatsAsString(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData(false, $this->dummy, 'enabled');
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBoolTrueAndFalseProduceDifferentOutput(): void
|
||||||
|
{
|
||||||
|
$true = self::$service->formatData(true, $this->dummy, 'enabled');
|
||||||
|
$false = self::$service->formatData(false, $this->dummy, 'enabled');
|
||||||
|
$this->assertNotSame($true, $false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIntegerFormatsToItsStringRepresentation(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData(42, $this->dummy, 'count');
|
||||||
|
$this->assertSame('42', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFloatFormatsToItsStringRepresentation(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData(3.14, $this->dummy, 'price');
|
||||||
|
$this->assertSame('3.14', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullFormatsAsItalicNull(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData(null, $this->dummy, 'field');
|
||||||
|
$this->assertSame('<i>null</i>', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDateTimeArrayFormatsToDateString(): void
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'date' => '2024-01-15 10:30:00.000000',
|
||||||
|
'timezone_type' => 3,
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
];
|
||||||
|
$result = self::$service->formatData($data, $this->dummy, 'created_at');
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
// Should not be the JSON fallback
|
||||||
|
$this->assertStringNotContainsString('json-formatter', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPlainArrayFormatsAsJsonDiv(): void
|
||||||
|
{
|
||||||
|
$result = self::$service->formatData(['key' => 'value', 'num' => 1], $this->dummy, 'tags');
|
||||||
|
$this->assertStringContainsString('json-formatter', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnsupportedTypeThrowsRuntimeException(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
self::$service->formatData(new \stdClass(), $this->dummy, 'field');
|
||||||
|
}
|
||||||
|
}
|
||||||
79
tests/Services/LogSystem/LogDiffFormatterTest.php
Normal file
79
tests/Services/LogSystem/LogDiffFormatterTest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\LogSystem;
|
||||||
|
|
||||||
|
use App\Services\LogSystem\LogDiffFormatter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class LogDiffFormatterTest extends TestCase
|
||||||
|
{
|
||||||
|
private LogDiffFormatter $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->service = new LogDiffFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPositiveNumericDiff(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->formatDiff(1, 6);
|
||||||
|
$this->assertStringContainsString('text-success', $result);
|
||||||
|
$this->assertStringContainsString('+5', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNegativeNumericDiff(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->formatDiff(10, 3);
|
||||||
|
$this->assertStringContainsString('text-danger', $result);
|
||||||
|
$this->assertStringContainsString('-7', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroNumericDiff(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->formatDiff(5, 5);
|
||||||
|
$this->assertStringContainsString('text-muted', $result);
|
||||||
|
$this->assertStringContainsString('0', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringDiffReturnsNonEmptyHtml(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->formatDiff('hello world', 'hello PHP');
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
// DiffHelper returns HTML
|
||||||
|
$this->assertStringContainsString('<', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnsupportedTypesReturnEmptyString(): void
|
||||||
|
{
|
||||||
|
// booleans are neither string nor numeric → empty
|
||||||
|
$result = $this->service->formatDiff(true, false);
|
||||||
|
$this->assertSame('', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFloatDiff(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->formatDiff(1.5, 3.0);
|
||||||
|
$this->assertStringContainsString('text-success', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
tests/Services/LogSystem/LogEntryExtraFormatterTest.php
Normal file
92
tests/Services/LogSystem/LogEntryExtraFormatterTest.php
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\LogSystem;
|
||||||
|
|
||||||
|
use App\Entity\LogSystem\DatabaseUpdatedLogEntry;
|
||||||
|
use App\Entity\LogSystem\UserLoginLogEntry;
|
||||||
|
use App\Entity\LogSystem\UserLogoutLogEntry;
|
||||||
|
use App\Services\LogSystem\LogEntryExtraFormatter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
final class LogEntryExtraFormatterTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static LogEntryExtraFormatter $service;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$service = self::getContainer()->get(LogEntryExtraFormatter::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatUserLoginLogEntryContainsIp(): void
|
||||||
|
{
|
||||||
|
$entry = new UserLoginLogEntry('127.0.0.1', anonymize: false);
|
||||||
|
$result = self::$service->format($entry);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertStringContainsString('127.0.0.1', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatDatabaseUpdatedLogEntryContainsVersions(): void
|
||||||
|
{
|
||||||
|
$entry = new DatabaseUpdatedLogEntry('1.0.0', '2.0.0');
|
||||||
|
$result = self::$service->format($entry);
|
||||||
|
$this->assertStringContainsString('1.0.0', $result);
|
||||||
|
$this->assertStringContainsString('2.0.0', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatUserLogoutContainsIp(): void
|
||||||
|
{
|
||||||
|
$entry = new UserLogoutLogEntry('10.0.0.1', anonymize: false);
|
||||||
|
$result = self::$service->format($entry);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertStringContainsString('10.0.0.1', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatConsoleReplacesHtmlTags(): void
|
||||||
|
{
|
||||||
|
$entry = new DatabaseUpdatedLogEntry('1.0', '2.0');
|
||||||
|
$result = self::$service->formatConsole($entry);
|
||||||
|
// Console format replaces the arrow icon with →
|
||||||
|
$this->assertStringContainsString('→', $result);
|
||||||
|
// No raw HTML tags should remain from the arrow icon
|
||||||
|
$this->assertStringNotContainsString('<i class="fas fa-long-arrow-alt-right"></i>', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFormatConsoleReturnsString(): void
|
||||||
|
{
|
||||||
|
$entry = new UserLoginLogEntry('192.168.1.1', anonymize: false);
|
||||||
|
$result = self::$service->formatConsole($entry);
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIpAddressIsHtmlEscapedInFormat(): void
|
||||||
|
{
|
||||||
|
// Verify that the IP embedded in the result is safe (htmlspecialchars is applied)
|
||||||
|
$entry = new UserLoginLogEntry('192.168.0.1', anonymize: false);
|
||||||
|
$result = self::$service->format($entry);
|
||||||
|
// The result must not contain unescaped HTML even from a crafted IP
|
||||||
|
$this->assertStringNotContainsString('<script>', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
tests/Services/LogSystem/LogLevelHelperTest.php
Normal file
85
tests/Services/LogSystem/LogLevelHelperTest.php
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\LogSystem;
|
||||||
|
|
||||||
|
use App\Services\LogSystem\LogLevelHelper;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LogLevel;
|
||||||
|
|
||||||
|
final class LogLevelHelperTest extends TestCase
|
||||||
|
{
|
||||||
|
private LogLevelHelper $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->service = new LogLevelHelper();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function iconClassProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield [LogLevel::DEBUG, 'fa-bug'];
|
||||||
|
yield [LogLevel::INFO, 'fa-info'];
|
||||||
|
yield [LogLevel::NOTICE, 'fa-flag'];
|
||||||
|
yield [LogLevel::WARNING, 'fa-exclamation-circle'];
|
||||||
|
yield [LogLevel::ERROR, 'fa-exclamation-triangle'];
|
||||||
|
yield [LogLevel::CRITICAL, 'fa-bolt'];
|
||||||
|
yield [LogLevel::ALERT, 'fa-radiation'];
|
||||||
|
yield [LogLevel::EMERGENCY, 'fa-skull-crossbones'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('iconClassProvider')]
|
||||||
|
public function testLogLevelToIconClass(string $logLevel, string $expectedIcon): void
|
||||||
|
{
|
||||||
|
$this->assertSame($expectedIcon, $this->service->logLevelToIconClass($logLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownLogLevelReturnsDefaultIcon(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('fa-question-circle', $this->service->logLevelToIconClass('unknown_level'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tableColorProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield [LogLevel::EMERGENCY, 'table-danger'];
|
||||||
|
yield [LogLevel::ALERT, 'table-danger'];
|
||||||
|
yield [LogLevel::CRITICAL, 'table-danger'];
|
||||||
|
yield [LogLevel::ERROR, 'table-danger'];
|
||||||
|
yield [LogLevel::WARNING, 'table-warning'];
|
||||||
|
yield [LogLevel::NOTICE, 'table-info'];
|
||||||
|
yield [LogLevel::INFO, ''];
|
||||||
|
yield [LogLevel::DEBUG, ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('tableColorProvider')]
|
||||||
|
public function testLogLevelToTableColorClass(string $logLevel, string $expectedClass): void
|
||||||
|
{
|
||||||
|
$this->assertSame($expectedClass, $this->service->logLevelToTableColorClass($logLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownLogLevelReturnsEmptyColor(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('', $this->service->logLevelToTableColorClass('unknown_level'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Tests\Services\Parts;
|
namespace App\Tests\Services\Parts;
|
||||||
|
|
||||||
|
use App\Entity\Parts\MeasurementUnit;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\PartLot;
|
use App\Entity\Parts\PartLot;
|
||||||
use App\Entity\Parts\StorageLocation;
|
use App\Entity\Parts\StorageLocation;
|
||||||
|
|
@ -167,6 +168,223 @@ final class PartLotWithdrawAddHelperTest extends WebTestCase
|
||||||
$this->service->stocktake($this->partLot2, 0, "Test");
|
$this->service->stocktake($this->partLot2, 0, "Test");
|
||||||
$this->assertEqualsWithDelta(0.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON);
|
$this->assertEqualsWithDelta(0.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
$this->assertFalse($this->partLot2->isInstockUnknown()); //Instock unknown should be cleared
|
$this->assertFalse($this->partLot2->isInstockUnknown()); //Instock unknown should be cleared
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- withdraw() error paths ---
|
||||||
|
|
||||||
|
public function testWithdrawZeroAmountThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->withdraw($this->partLot1, 0, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithdrawNegativeAmountThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->withdraw($this->partLot1, -5, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithdrawMoreThanStockThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->withdraw($this->partLot1, 999, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithdrawFromUnknownInstockLotThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->withdraw($this->lotWithUnknownInstock, 1, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- add() error paths ---
|
||||||
|
|
||||||
|
public function testAddZeroAmountThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->add($this->partLot1, 0, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddNegativeAmountThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->add($this->partLot1, -3, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddToFullLotThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->add($this->fullLot, 1, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddToUnknownInstockLotThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->add($this->lotWithUnknownInstock, 1, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- move() error paths ---
|
||||||
|
|
||||||
|
public function testMoveZeroAmountThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->move($this->partLot1, $this->partLot2, 0, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMoveBetweenDifferentPartsThrows(): void
|
||||||
|
{
|
||||||
|
$otherPart = new Part();
|
||||||
|
$otherLot = new TestPartLot();
|
||||||
|
$otherLot->setPart($otherPart);
|
||||||
|
$otherLot->setAmount(5);
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->move($this->partLot1, $otherLot, 5, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMoveMoreThanOriginStockThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->move($this->partLot1, $this->partLot2, 999, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMoveFromUnwithdrawableLotThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->move($this->lotWithUnknownInstock, $this->partLot2, 1, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMoveToUnavailableLotThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->service->move($this->partLot1, $this->fullLot, 1, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- stocktake() error paths ---
|
||||||
|
|
||||||
|
public function testStocktakeNegativeAmountThrows(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->stocktake($this->partLot1, -1, "Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- integer-rounding (useFloatAmount() = false, no unit set) ---
|
||||||
|
|
||||||
|
public function testWithdrawRoundsAmountForIntegerPart(): void
|
||||||
|
{
|
||||||
|
// No unit → useFloatAmount() = false → fractional amounts are rounded
|
||||||
|
$this->assertFalse($this->part->useFloatAmount());
|
||||||
|
|
||||||
|
$this->service->withdraw($this->partLot1, 1.7, "Test"); // rounds to 2
|
||||||
|
$this->assertEqualsWithDelta(8.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddRoundsAmountForIntegerPart(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->part->useFloatAmount());
|
||||||
|
|
||||||
|
$this->service->add($this->partLot3, 1.7, "Test"); // rounds to 2
|
||||||
|
$this->assertEqualsWithDelta(2.0, $this->partLot3->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStocktakeRoundsAmountForIntegerPart(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->part->useFloatAmount());
|
||||||
|
|
||||||
|
$this->service->stocktake($this->partLot1, 7.6, "Test"); // rounds to 8
|
||||||
|
$this->assertEqualsWithDelta(8.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- float amounts are preserved when the unit allows floats ---
|
||||||
|
|
||||||
|
public function testAddPreservesFloatAmountForFloatUnit(): void
|
||||||
|
{
|
||||||
|
$unit = new MeasurementUnit();
|
||||||
|
$unit->setIsInteger(false);
|
||||||
|
|
||||||
|
$floatPart = new Part();
|
||||||
|
$floatPart->setPartUnit($unit);
|
||||||
|
$this->assertTrue($floatPart->useFloatAmount());
|
||||||
|
|
||||||
|
$lot = new TestPartLot();
|
||||||
|
$lot->setPart($floatPart);
|
||||||
|
$lot->setAmount(1.0);
|
||||||
|
|
||||||
|
$this->service->add($lot, 1.3, "Test");
|
||||||
|
$this->assertEqualsWithDelta(2.3, $lot->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithdrawPreservesFloatAmountForFloatUnit(): void
|
||||||
|
{
|
||||||
|
$unit = new MeasurementUnit();
|
||||||
|
$unit->setIsInteger(false);
|
||||||
|
|
||||||
|
$floatPart = new Part();
|
||||||
|
$floatPart->setPartUnit($unit);
|
||||||
|
|
||||||
|
$lot = new TestPartLot();
|
||||||
|
$lot->setPart($floatPart);
|
||||||
|
$lot->setAmount(5.0);
|
||||||
|
|
||||||
|
$this->service->withdraw($lot, 1.3, "Test");
|
||||||
|
$this->assertEqualsWithDelta(3.7, $lot->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- delete_lot_if_empty ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a PartLot that looks like a managed, persisted entity to Doctrine:
|
||||||
|
* - has a non-null ID (required by AbstractLogEntry when creating stock-change log entries)
|
||||||
|
* - is registered in the UnitOfWork as managed (required so EntityManager::remove() accepts it)
|
||||||
|
*/
|
||||||
|
private function makeManagedLot(float $amount, int $fakeId = 42): PartLot
|
||||||
|
{
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setPart($this->part);
|
||||||
|
$lot->setAmount($amount);
|
||||||
|
|
||||||
|
$ref = new \ReflectionProperty($lot, 'id');
|
||||||
|
$ref->setValue($lot, $fakeId);
|
||||||
|
|
||||||
|
$em = self::getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$em->getUnitOfWork()->registerManaged($lot, ['id' => $fakeId], []);
|
||||||
|
|
||||||
|
return $lot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithdrawDeletesLotWhenEmptyAndFlagSet(): void
|
||||||
|
{
|
||||||
|
$lot = $this->makeManagedLot(10);
|
||||||
|
|
||||||
|
$this->service->withdraw($lot, 10, "Test", null, true);
|
||||||
|
$this->assertEqualsWithDelta(0.0, $lot->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
|
||||||
|
$em = self::getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$scheduled = $em->getUnitOfWork()->getScheduledEntityDeletions();
|
||||||
|
$this->assertContains($lot, $scheduled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithdrawDoesNotDeleteLotWhenNotEmptyAndFlagSet(): void
|
||||||
|
{
|
||||||
|
$lot = $this->makeManagedLot(10);
|
||||||
|
|
||||||
|
$this->service->withdraw($lot, 5, "Test", null, true);
|
||||||
|
$this->assertEqualsWithDelta(5.0, $lot->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
|
||||||
|
$em = self::getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$scheduled = $em->getUnitOfWork()->getScheduledEntityDeletions();
|
||||||
|
$this->assertNotContains($lot, $scheduled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMoveDeletesOriginLotWhenEmptyAndFlagSet(): void
|
||||||
|
{
|
||||||
|
$origin = $this->makeManagedLot(10, 43);
|
||||||
|
$target = $this->makeManagedLot(0, 44);
|
||||||
|
|
||||||
|
$this->service->move($origin, $target, 10, "Test", null, true);
|
||||||
|
$this->assertEqualsWithDelta(0.0, $origin->getAmount(), PHP_FLOAT_EPSILON);
|
||||||
|
|
||||||
|
$em = self::getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$scheduled = $em->getUnitOfWork()->getScheduledEntityDeletions();
|
||||||
|
$this->assertContains($origin, $scheduled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,20 +43,52 @@ final class PartsTableActionHandlerTest extends WebTestCase
|
||||||
$part = $this->createMock(Part::class);
|
$part = $this->createMock(Part::class);
|
||||||
$part->method('getId')->willReturn(1);
|
$part->method('getId')->willReturn(1);
|
||||||
$part->method('getName')->willReturn('Test Part');
|
$part->method('getName')->willReturn('Test Part');
|
||||||
|
|
||||||
$selected_parts = [$part];
|
$selected_parts = [$part];
|
||||||
|
|
||||||
// Test each export format, focusing on our new xlsx format
|
// Test each export format, focusing on our new xlsx format
|
||||||
$formats = ['json', 'csv', 'xml', 'yaml', 'xlsx'];
|
$formats = ['json', 'csv', 'xml', 'yaml', 'xlsx'];
|
||||||
|
|
||||||
foreach ($formats as $format) {
|
foreach ($formats as $format) {
|
||||||
$action = "export_{$format}";
|
$action = "export_{$format}";
|
||||||
$result = $this->service->handleAction($action, $selected_parts, 1, '/test');
|
$result = $this->service->handleAction($action, $selected_parts, 1, '/test');
|
||||||
|
|
||||||
$this->assertInstanceOf(RedirectResponse::class, $result);
|
$this->assertInstanceOf(RedirectResponse::class, $result);
|
||||||
$this->assertStringContainsString('parts/export', $result->getTargetUrl());
|
$this->assertStringContainsString('parts/export', $result->getTargetUrl());
|
||||||
$this->assertStringContainsString("format={$format}", $result->getTargetUrl());
|
$this->assertStringContainsString("format={$format}", $result->getTargetUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testExportUrlContainsPartIds(): void
|
||||||
|
{
|
||||||
|
$part1 = $this->createMock(Part::class);
|
||||||
|
$part1->method('getId')->willReturn(42);
|
||||||
|
|
||||||
|
$part2 = $this->createMock(Part::class);
|
||||||
|
$part2->method('getId')->willReturn(99);
|
||||||
|
|
||||||
|
$result = $this->service->handleAction('export_csv', [$part1, $part2], 1, '/test');
|
||||||
|
|
||||||
|
$this->assertInstanceOf(RedirectResponse::class, $result);
|
||||||
|
// Commas in query-string values are not percent-encoded by Symfony's UrlGenerator
|
||||||
|
$this->assertStringContainsString('ids=42,99', $result->getTargetUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExportWithNoPartsProducesEmptyIds(): void
|
||||||
|
{
|
||||||
|
$result = $this->service->handleAction('export_json', [], 1, '/test');
|
||||||
|
|
||||||
|
$this->assertInstanceOf(RedirectResponse::class, $result);
|
||||||
|
$this->assertStringContainsString('parts/export', $result->getTargetUrl());
|
||||||
|
// ids parameter present but empty
|
||||||
|
$this->assertStringContainsString('ids=', $result->getTargetUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownActionWithEmptyPartsReturnsNull(): void
|
||||||
|
{
|
||||||
|
// The unknown-action switch only runs inside the foreach loop, so an
|
||||||
|
// empty parts list means the loop body never executes and no exception is thrown.
|
||||||
|
$result = $this->service->handleAction('unknown_action_xyz', [], null, '/test');
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -24,10 +24,12 @@ namespace App\Tests\Services\Parts;
|
||||||
|
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Entity\PriceInformations\Currency;
|
||||||
use App\Entity\PriceInformations\Orderdetail;
|
use App\Entity\PriceInformations\Orderdetail;
|
||||||
use App\Entity\PriceInformations\Pricedetail;
|
use App\Entity\PriceInformations\Pricedetail;
|
||||||
use App\Services\Formatters\AmountFormatter;
|
use App\Services\Formatters\AmountFormatter;
|
||||||
use App\Services\Parts\PricedetailHelper;
|
use App\Services\Parts\PricedetailHelper;
|
||||||
|
use Brick\Math\BigDecimal;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
final class PricedetailHelperTest extends WebTestCase
|
final class PricedetailHelperTest extends WebTestCase
|
||||||
|
|
@ -87,4 +89,181 @@ final class PricedetailHelperTest extends WebTestCase
|
||||||
{
|
{
|
||||||
$this->assertSame($expected_result, $this->service->getMaxDiscountAmount($part), $message);
|
$this->assertSame($expected_result, $this->service->getMaxDiscountAmount($part), $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- getMinOrderAmount ---
|
||||||
|
|
||||||
|
public static function minOrderAmountDataProvider(): \Generator
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
yield [$part, null, 'No orderdetails'];
|
||||||
|
|
||||||
|
$part = new Part();
|
||||||
|
$part->addOrderdetail(new Orderdetail()); // orderdetail with no pricedetails
|
||||||
|
yield [$part, null, 'Empty orderdetail'];
|
||||||
|
|
||||||
|
$part = new Part();
|
||||||
|
$od = new Orderdetail();
|
||||||
|
$od->addPricedetail((new Pricedetail())->setMinDiscountQuantity(5));
|
||||||
|
$part->addOrderdetail($od);
|
||||||
|
yield [$part, 5.0, 'Single pricedetail'];
|
||||||
|
|
||||||
|
// The service reads $pricedetails[0] assuming the collection is sorted ascending
|
||||||
|
// (which Doctrine does automatically for persistent collections). For in-memory
|
||||||
|
// collections we must insert in ascending order ourselves.
|
||||||
|
$part = new Part();
|
||||||
|
$od = new Orderdetail();
|
||||||
|
$od->addPricedetail((new Pricedetail())->setMinDiscountQuantity(1));
|
||||||
|
$od->addPricedetail((new Pricedetail())->setMinDiscountQuantity(3));
|
||||||
|
$od->addPricedetail((new Pricedetail())->setMinDiscountQuantity(10));
|
||||||
|
$part->addOrderdetail($od);
|
||||||
|
yield [$part, 1.0, 'Multiple pricedetails — picks minimum (first in ascending order)'];
|
||||||
|
|
||||||
|
$part = new Part();
|
||||||
|
$od1 = new Orderdetail();
|
||||||
|
$od1->addPricedetail((new Pricedetail())->setMinDiscountQuantity(5));
|
||||||
|
$od2 = new Orderdetail();
|
||||||
|
$od2->addPricedetail((new Pricedetail())->setMinDiscountQuantity(2));
|
||||||
|
$part->addOrderdetail($od1);
|
||||||
|
$part->addOrderdetail($od2);
|
||||||
|
yield [$part, 2.0, 'Multiple orderdetails — picks global minimum'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('minOrderAmountDataProvider')]
|
||||||
|
public function testGetMinOrderAmount(Part $part, ?float $expected, string $message): void
|
||||||
|
{
|
||||||
|
$this->assertSame($expected, $this->service->getMinOrderAmount($part), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- calculateAvgPrice ---
|
||||||
|
|
||||||
|
private static function makePartWithPrice(float $pricePerUnit, float $minQty = 1.0): Part
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
$od = new Orderdetail();
|
||||||
|
$pd = (new Pricedetail())
|
||||||
|
->setMinDiscountQuantity($minQty)
|
||||||
|
->setPrice(BigDecimal::of((string) $pricePerUnit));
|
||||||
|
$od->addPricedetail($pd);
|
||||||
|
$part->addOrderdetail($od);
|
||||||
|
return $part;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateAvgPriceNoOrderdetailsReturnsNull(): void
|
||||||
|
{
|
||||||
|
$this->assertNull($this->service->calculateAvgPrice(new Part()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateAvgPriceExplicitAmount(): void
|
||||||
|
{
|
||||||
|
$part = self::makePartWithPrice(2.00);
|
||||||
|
$result = $this->service->calculateAvgPrice($part, 1.0);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertTrue(BigDecimal::of('2.00000')->isEqualTo($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateAvgPriceUsesMinOrderAmountWhenAmountIsNull(): void
|
||||||
|
{
|
||||||
|
// Min order amount is 5; the price applies for qty >= 5
|
||||||
|
$part = self::makePartWithPrice(3.00, 5.0);
|
||||||
|
$result = $this->service->calculateAvgPrice($part, null);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertTrue(BigDecimal::of('3.00000')->isEqualTo($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateAvgPriceAveragesMultipleSuppliers(): void
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
|
||||||
|
$od1 = new Orderdetail();
|
||||||
|
$od1->addPricedetail((new Pricedetail())->setMinDiscountQuantity(1)->setPrice(BigDecimal::of('2.00')));
|
||||||
|
$part->addOrderdetail($od1);
|
||||||
|
|
||||||
|
$od2 = new Orderdetail();
|
||||||
|
$od2->addPricedetail((new Pricedetail())->setMinDiscountQuantity(1)->setPrice(BigDecimal::of('4.00')));
|
||||||
|
$part->addOrderdetail($od2);
|
||||||
|
|
||||||
|
// Average of 2.00 and 4.00 = 3.00
|
||||||
|
$result = $this->service->calculateAvgPrice($part, 1.0);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertTrue(BigDecimal::of('3.00000')->isEqualTo($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateAvgPriceSkipsSupplierWithNoCoverageForAmount(): void
|
||||||
|
{
|
||||||
|
// Only one supplier covers qty=1, the other requires qty >= 100
|
||||||
|
$part = new Part();
|
||||||
|
$od1 = new Orderdetail();
|
||||||
|
$od1->addPricedetail((new Pricedetail())->setMinDiscountQuantity(1)->setPrice(BigDecimal::of('5.00')));
|
||||||
|
$part->addOrderdetail($od1);
|
||||||
|
|
||||||
|
$od2 = new Orderdetail();
|
||||||
|
$od2->addPricedetail((new Pricedetail())->setMinDiscountQuantity(100)->setPrice(BigDecimal::of('1.00')));
|
||||||
|
$part->addOrderdetail($od2);
|
||||||
|
|
||||||
|
$result = $this->service->calculateAvgPrice($part, 1.0);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertTrue(BigDecimal::of('5.00000')->isEqualTo($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- convertMoneyToCurrency ---
|
||||||
|
|
||||||
|
public function testConvertMoneyToCurrencyIdentityBothNull(): void
|
||||||
|
{
|
||||||
|
// Both currencies null = base currency; same currency, no conversion
|
||||||
|
$value = BigDecimal::of('10.00');
|
||||||
|
$result = $this->service->convertMoneyToCurrency($value, null, null);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertTrue($value->isEqualTo($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConvertMoneyToCurrencyFromForeignToBase(): void
|
||||||
|
{
|
||||||
|
// EUR → base (null): exchange rate = 1.2 means 1 foreign = 1.2 base
|
||||||
|
$currency = new Currency();
|
||||||
|
$currency->setExchangeRate(BigDecimal::of('1.2'));
|
||||||
|
|
||||||
|
$result = $this->service->convertMoneyToCurrency(BigDecimal::of('10.00'), $currency, null);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
// 10 * 1.2 = 12
|
||||||
|
$this->assertTrue(BigDecimal::of('12.00000')->isEqualTo($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConvertMoneyToCurrencyNullExchangeRateReturnsNull(): void
|
||||||
|
{
|
||||||
|
$currency = new Currency();
|
||||||
|
// exchange rate not set → null
|
||||||
|
|
||||||
|
$result = $this->service->convertMoneyToCurrency(BigDecimal::of('10.00'), $currency, null);
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConvertMoneyToCurrencyZeroExchangeRateReturnsNull(): void
|
||||||
|
{
|
||||||
|
$currency = new Currency();
|
||||||
|
$currency->setExchangeRate(BigDecimal::zero());
|
||||||
|
|
||||||
|
$result = $this->service->convertMoneyToCurrency(BigDecimal::of('10.00'), $currency, null);
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConvertMoneyToCurrencyTargetNullExchangeRateReturnsNull(): void
|
||||||
|
{
|
||||||
|
$target = new Currency();
|
||||||
|
// exchange rate not set → getInverseExchangeRate() returns null
|
||||||
|
|
||||||
|
$result = $this->service->convertMoneyToCurrency(BigDecimal::of('10.00'), null, $target);
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConvertMoneyToCurrencySameCurrencyInstanceIsIdentity(): void
|
||||||
|
{
|
||||||
|
$currency = new Currency();
|
||||||
|
$currency->setExchangeRate(BigDecimal::of('2.0'));
|
||||||
|
|
||||||
|
$value = BigDecimal::of('5.00');
|
||||||
|
// origin === target → no conversion at all
|
||||||
|
$result = $this->service->convertMoneyToCurrency($value, $currency, $currency);
|
||||||
|
$this->assertNotNull($result);
|
||||||
|
$this->assertTrue($value->isEqualTo($result));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,132 @@ final class ProjectBuildHelperTest extends WebTestCase
|
||||||
$this->assertTrue(BigDecimal::of('0.01')->isEqualTo($result));
|
$this->assertTrue(BigDecimal::of('0.01')->isEqualTo($result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- unknown-instock lots are excluded from buildable count ---
|
||||||
|
|
||||||
|
public function testGetMaximumBuildableCountForBOMEntryExcludesUnknownInstockLots(): void
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setAmount(100);
|
||||||
|
$lot->setInstockUnknown(true); // this lot should be ignored
|
||||||
|
$part->addPartLot($lot);
|
||||||
|
|
||||||
|
$entry = (new ProjectBOMEntry())->setPart($part)->setQuantity(10);
|
||||||
|
|
||||||
|
// All stock is in an unknown-instock lot → effective amount = 0 → 0 builds
|
||||||
|
$this->assertSame(0, $this->service->getMaximumBuildableCountForBOMEntry($entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetMaximumBuildableCountMixedKnownAndUnknownLots(): void
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
|
||||||
|
$knownLot = new PartLot();
|
||||||
|
$knownLot->setAmount(30);
|
||||||
|
|
||||||
|
$unknownLot = new PartLot();
|
||||||
|
$unknownLot->setAmount(999);
|
||||||
|
$unknownLot->setInstockUnknown(true);
|
||||||
|
|
||||||
|
$part->addPartLot($knownLot);
|
||||||
|
$part->addPartLot($unknownLot);
|
||||||
|
|
||||||
|
$entry = (new ProjectBOMEntry())->setPart($part)->setQuantity(10);
|
||||||
|
|
||||||
|
// Only the 30 known parts count → floor(30/10) = 3
|
||||||
|
$this->assertSame(3, $this->service->getMaximumBuildableCountForBOMEntry($entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- project with only non-part BOM entries ---
|
||||||
|
|
||||||
|
public function testGetMaximumBuildableCountOnlyNonPartEntriesReturnsIntMax(): void
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
$project->addBomEntry((new ProjectBOMEntry())->setName('Solder')->setQuantity(1));
|
||||||
|
$project->addBomEntry((new ProjectBOMEntry())->setName('Wire')->setQuantity(2));
|
||||||
|
|
||||||
|
// No part entries → nothing constrains the count → PHP_INT_MAX
|
||||||
|
$this->assertSame(PHP_INT_MAX, $this->service->getMaximumBuildableCount($project));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetMaximumBuildableCountAsStringOnlyNonPartEntries(): void
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
$project->addBomEntry((new ProjectBOMEntry())->setName('Solder')->setQuantity(1));
|
||||||
|
|
||||||
|
$this->assertSame('∞', $this->service->getMaximumBuildableCountAsString($project));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- isProjectBuildable ---
|
||||||
|
|
||||||
|
public function testIsProjectBuildable(): void
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
$part = new Part();
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setAmount(15);
|
||||||
|
$part->addPartLot($lot);
|
||||||
|
$project->addBomEntry((new ProjectBOMEntry())->setPart($part)->setQuantity(5));
|
||||||
|
|
||||||
|
$this->assertTrue($this->service->isProjectBuildable($project, 3)); // 15/5 = 3 ✓
|
||||||
|
$this->assertFalse($this->service->isProjectBuildable($project, 4)); // 4 > 3 ✗
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- isBOMEntryBuildable ---
|
||||||
|
|
||||||
|
public function testIsBOMEntryBuildable(): void
|
||||||
|
{
|
||||||
|
$part = new Part();
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setAmount(20);
|
||||||
|
$part->addPartLot($lot);
|
||||||
|
|
||||||
|
$entry = (new ProjectBOMEntry())->setPart($part)->setQuantity(10);
|
||||||
|
|
||||||
|
$this->assertTrue($this->service->isBOMEntryBuildable($entry, 2)); // 20/10 = 2 ✓
|
||||||
|
$this->assertFalse($this->service->isBOMEntryBuildable($entry, 3)); // 3 > 2 ✗
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- getNonBuildableProjectBomEntries ---
|
||||||
|
|
||||||
|
public function testGetNonBuildableProjectBomEntriesReturnsShortEntries(): void
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
|
||||||
|
$abundantPart = new Part();
|
||||||
|
$lot1 = new PartLot();
|
||||||
|
$lot1->setAmount(100);
|
||||||
|
$abundantPart->addPartLot($lot1);
|
||||||
|
$project->addBomEntry((new ProjectBOMEntry())->setPart($abundantPart)->setQuantity(5));
|
||||||
|
|
||||||
|
$scarcePart = new Part();
|
||||||
|
$lot2 = new PartLot();
|
||||||
|
$lot2->setAmount(3);
|
||||||
|
$scarcePart->addPartLot($lot2);
|
||||||
|
$scarceEntry = (new ProjectBOMEntry())->setPart($scarcePart)->setQuantity(10);
|
||||||
|
$project->addBomEntry($scarceEntry);
|
||||||
|
|
||||||
|
// For 1 build: abundantPart OK (100 >= 5), scarcePart not (3 < 10)
|
||||||
|
$nonBuildable = $this->service->getNonBuildableProjectBomEntries($project, 1);
|
||||||
|
$this->assertCount(1, $nonBuildable);
|
||||||
|
$this->assertSame($scarceEntry, $nonBuildable[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetNonBuildableProjectBomEntriesSkipsNonPartEntries(): void
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
$project->addBomEntry((new ProjectBOMEntry())->setName('Wire')->setQuantity(5));
|
||||||
|
|
||||||
|
// Non-part entries are ignored → no non-buildable entries
|
||||||
|
$this->assertCount(0, $this->service->getNonBuildableProjectBomEntries($project, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetNonBuildableProjectBomEntriesThrowsOnZeroBuilds(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->getNonBuildableProjectBomEntries(new Project(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function testCalculateTotalBuildPriceMixedEntries(): void
|
public function testCalculateTotalBuildPriceMixedEntries(): void
|
||||||
{
|
{
|
||||||
$project = new Project();
|
$project = new Project();
|
||||||
|
|
|
||||||
105
tests/Services/UserSystem/PermissionPresetsHelperTest.php
Normal file
105
tests/Services/UserSystem/PermissionPresetsHelperTest.php
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\UserSystem;
|
||||||
|
|
||||||
|
use App\Entity\UserSystem\User;
|
||||||
|
use App\Services\UserSystem\PermissionManager;
|
||||||
|
use App\Services\UserSystem\PermissionPresetsHelper;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
|
||||||
|
final class PermissionPresetsHelperTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static PermissionPresetsHelper $service;
|
||||||
|
private static PermissionManager $permissionManager;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$service = self::getContainer()->get(PermissionPresetsHelper::class);
|
||||||
|
self::$permissionManager = self::getContainer()->get(PermissionManager::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createUser(): User
|
||||||
|
{
|
||||||
|
return new User();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllInheritPresetLeavesAllPermissionsInherit(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
self::$service->applyPreset($user, PermissionPresetsHelper::PRESET_ALL_INHERIT);
|
||||||
|
|
||||||
|
// After all-inherit preset, 'parts' read should be null (inherit)
|
||||||
|
$this->assertNull(self::$permissionManager->dontInherit($user, 'parts', 'read'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllForbidPresetSetsAllPermissionsToFalse(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
self::$service->applyPreset($user, PermissionPresetsHelper::PRESET_ALL_FORBID);
|
||||||
|
|
||||||
|
// After all-forbid, 'parts' read should be false (disallowed)
|
||||||
|
$this->assertFalse(self::$permissionManager->dontInherit($user, 'parts', 'read'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllAllowPresetSetsAllPermissionsToTrue(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
self::$service->applyPreset($user, PermissionPresetsHelper::PRESET_ALL_ALLOW);
|
||||||
|
|
||||||
|
// After all-allow, 'parts' read should be true (allowed)
|
||||||
|
$this->assertTrue(self::$permissionManager->dontInherit($user, 'parts', 'read'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadOnlyPresetAllowsPartsRead(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
self::$service->applyPreset($user, PermissionPresetsHelper::PRESET_READ_ONLY);
|
||||||
|
|
||||||
|
$this->assertTrue(self::$permissionManager->dontInherit($user, 'parts', 'read'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadOnlyPresetDoesNotAllowPartsCreate(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
self::$service->applyPreset($user, PermissionPresetsHelper::PRESET_READ_ONLY);
|
||||||
|
|
||||||
|
// create should remain null (inherit) or false — not explicitly allowed
|
||||||
|
$createValue = self::$permissionManager->dontInherit($user, 'parts', 'create');
|
||||||
|
$this->assertNotTrue($createValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownPresetThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
self::$service->applyPreset($this->createUser(), 'non_existent_preset');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApplyPresetReturnsTheSameUser(): void
|
||||||
|
{
|
||||||
|
$user = $this->createUser();
|
||||||
|
$returned = self::$service->applyPreset($user, PermissionPresetsHelper::PRESET_ALL_INHERIT);
|
||||||
|
$this->assertSame($user, $returned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints\BigDecimal;
|
||||||
|
|
||||||
|
use App\Validator\Constraints\BigDecimal\BigDecimalGreaterThanValidator;
|
||||||
|
use App\Validator\Constraints\BigDecimal\BigDecimalPositive;
|
||||||
|
use Brick\Math\BigDecimal;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests BigDecimalGreaterThanValidator via the BigDecimalPositive constraint (value > 0).
|
||||||
|
*/
|
||||||
|
final class BigDecimalGreaterThanValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator(): ConstraintValidatorInterface
|
||||||
|
{
|
||||||
|
return new BigDecimalGreaterThanValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(null, new BigDecimalPositive());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPositiveIntegerIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(1, new BigDecimalPositive());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPositiveStringIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate('0.01', new BigDecimalPositive());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPositiveBigDecimalIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(BigDecimal::of('1.5'), new BigDecimalPositive());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroIsInvalid(): void
|
||||||
|
{
|
||||||
|
$constraint = new BigDecimalPositive();
|
||||||
|
$this->validator->validate(0, $constraint);
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameters(['{{ value }}' => '0', '{{ compared_value }}' => '0', '{{ compared_value_type }}' => 'int'])
|
||||||
|
->setCode(\Symfony\Component\Validator\Constraints\GreaterThan::TOO_LOW_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroBigDecimalIsInvalid(): void
|
||||||
|
{
|
||||||
|
$constraint = new BigDecimalPositive();
|
||||||
|
$this->validator->validate(BigDecimal::of('0.00'), $constraint);
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameters(['{{ value }}' => '0.00', '{{ compared_value }}' => '0', '{{ compared_value_type }}' => 'int'])
|
||||||
|
->setCode(\Symfony\Component\Validator\Constraints\GreaterThan::TOO_LOW_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNegativeIsInvalid(): void
|
||||||
|
{
|
||||||
|
$constraint = new BigDecimalPositive();
|
||||||
|
$this->validator->validate(-1, $constraint);
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameters(['{{ value }}' => '-1', '{{ compared_value }}' => '0', '{{ compared_value_type }}' => 'int'])
|
||||||
|
->setCode(\Symfony\Component\Validator\Constraints\GreaterThan::TOO_LOW_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints\BigDecimal;
|
||||||
|
|
||||||
|
use App\Validator\Constraints\BigDecimal\BigDecimalGreaterThenOrEqualValidator;
|
||||||
|
use App\Validator\Constraints\BigDecimal\BigDecimalPositiveOrZero;
|
||||||
|
use Brick\Math\BigDecimal;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests BigDecimalGreaterThenOrEqualValidator via the BigDecimalPositiveOrZero constraint (value >= 0).
|
||||||
|
*/
|
||||||
|
final class BigDecimalGreaterThenOrEqualValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator(): ConstraintValidatorInterface
|
||||||
|
{
|
||||||
|
return new BigDecimalGreaterThenOrEqualValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(null, new BigDecimalPositiveOrZero());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPositiveIntegerIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(1, new BigDecimalPositiveOrZero());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(0, new BigDecimalPositiveOrZero());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroBigDecimalIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(BigDecimal::of('0.00'), new BigDecimalPositiveOrZero());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPositiveBigDecimalIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(BigDecimal::of('3.14'), new BigDecimalPositiveOrZero());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNegativeIsInvalid(): void
|
||||||
|
{
|
||||||
|
$constraint = new BigDecimalPositiveOrZero();
|
||||||
|
$this->validator->validate(-1, $constraint);
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameters(['{{ value }}' => '-1', '{{ compared_value }}' => '0', '{{ compared_value_type }}' => 'int'])
|
||||||
|
->setCode(\Symfony\Component\Validator\Constraints\GreaterThanOrEqual::TOO_LOW_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNegativeBigDecimalIsInvalid(): void
|
||||||
|
{
|
||||||
|
$constraint = new BigDecimalPositiveOrZero();
|
||||||
|
$this->validator->validate(BigDecimal::of('-0.01'), $constraint);
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameters(['{{ value }}' => '-0.01', '{{ compared_value }}' => '0', '{{ compared_value_type }}' => 'int'])
|
||||||
|
->setCode(\Symfony\Component\Validator\Constraints\GreaterThanOrEqual::TOO_LOW_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -154,6 +154,33 @@ final class UniqueObjectCollectionValidatorTest extends ConstraintValidatorTestC
|
||||||
->assertRaised();
|
->assertRaised();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testThirdElementDuplicatePointsToIndexTwo(): void
|
||||||
|
{
|
||||||
|
// First two elements are unique; only the third duplicates the first.
|
||||||
|
$this->validator->validate(new ArrayCollection([
|
||||||
|
new DummyUniqueValidatableObject(['a' => 1]),
|
||||||
|
new DummyUniqueValidatableObject(['a' => 2]),
|
||||||
|
new DummyUniqueValidatableObject(['a' => 1]), // duplicate of index 0
|
||||||
|
]),
|
||||||
|
new UniqueObjectCollection(fields: ['a']));
|
||||||
|
|
||||||
|
$this
|
||||||
|
->buildViolation('This value is already used.')
|
||||||
|
->setCode(UniqueObjectCollection::IS_NOT_UNIQUE)
|
||||||
|
->setParameter('{{ object }}', 'objectString')
|
||||||
|
->atPath('property.path[2].a')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllNullsWithAllowNullProducesNoViolation(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(new ArrayCollection([
|
||||||
|
new DummyUniqueValidatableObject(['a' => null]),
|
||||||
|
new DummyUniqueValidatableObject(['a' => null]),
|
||||||
|
new DummyUniqueValidatableObject(['a' => null]),
|
||||||
|
]),
|
||||||
|
new UniqueObjectCollection(fields: ['a'], allowNull: true));
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
103
tests/Validator/Constraints/UniquePartIpnValidatorTest.php
Normal file
103
tests/Validator/Constraints/UniquePartIpnValidatorTest.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Entity\Base\AbstractDBElement;
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Settings\MiscSettings\IpnSuggestSettings;
|
||||||
|
use App\Validator\Constraints\UniquePartIpnConstraint;
|
||||||
|
use App\Validator\Constraints\UniquePartIpnValidator;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
final class UniquePartIpnValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
private EntityManagerInterface&MockObject $em;
|
||||||
|
private IpnSuggestSettings&MockObject $ipnSettings;
|
||||||
|
|
||||||
|
protected function createValidator(): ConstraintValidatorInterface
|
||||||
|
{
|
||||||
|
$this->em = $this->createMock(EntityManagerInterface::class);
|
||||||
|
// createMock() bypasses the ForbidConstructorTrait; public properties are accessible directly
|
||||||
|
$this->ipnSettings = $this->createMock(IpnSuggestSettings::class);
|
||||||
|
$this->ipnSettings->autoAppendSuffix = false;
|
||||||
|
|
||||||
|
return new UniquePartIpnValidator($this->em, $this->ipnSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullValueIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(null, new UniquePartIpnConstraint());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyStringIsValid(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate('', new UniquePartIpnConstraint());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAutoAppendSuffixSkipsValidation(): void
|
||||||
|
{
|
||||||
|
$this->ipnSettings->autoAppendSuffix = true;
|
||||||
|
$this->validator->validate('IPN-001', new UniquePartIpnConstraint());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueIpnIsValid(): void
|
||||||
|
{
|
||||||
|
$repo = $this->createMock(\Doctrine\ORM\EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([]);
|
||||||
|
$this->em->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
$part = new Part();
|
||||||
|
$this->setObject($part);
|
||||||
|
|
||||||
|
$this->validator->validate('UNIQUE-IPN', new UniquePartIpnConstraint());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDuplicateIpnRaisesViolation(): void
|
||||||
|
{
|
||||||
|
$existingPart = new Part();
|
||||||
|
$ref = new \ReflectionProperty(AbstractDBElement::class, 'id');
|
||||||
|
$ref->setValue($existingPart, 99);
|
||||||
|
|
||||||
|
$repo = $this->createMock(\Doctrine\ORM\EntityRepository::class);
|
||||||
|
$repo->method('findBy')->willReturn([$existingPart]);
|
||||||
|
$this->em->method('getRepository')->willReturn($repo);
|
||||||
|
|
||||||
|
// Validated part has no ID (new, unsaved part)
|
||||||
|
$part = new Part();
|
||||||
|
$this->setObject($part);
|
||||||
|
|
||||||
|
$constraint = new UniquePartIpnConstraint();
|
||||||
|
$this->validator->validate('DUPLICATE-IPN', $constraint);
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameter('{{ value }}', 'DUPLICATE-IPN')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
tests/Validator/Constraints/ValidFileFilterValidatorTest.php
Normal file
81
tests/Validator/Constraints/ValidFileFilterValidatorTest.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Validator\Constraints\ValidFileFilter;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
|
final class ValidFileFilterValidatorTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static ValidatorInterface $validator;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$validator = self::getContainer()->get('validator');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullIsValid(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate(null, new ValidFileFilter());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyStringIsValid(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate('', new ValidFileFilter());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidExtensionFilterIsValid(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate('.jpg,.png', new ValidFileFilter());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidMimeTypeFilterIsValid(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate('image/*', new ValidFileFilter());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMixedValidFilterIsValid(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate('image/*, .pdf, video/mp4', new ValidFileFilter());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidFilterRaisesViolation(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate('*.notvalid', new ValidFileFilter());
|
||||||
|
$this->assertCount(1, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFullFilenameRaisesViolation(): void
|
||||||
|
{
|
||||||
|
$violations = self::$validator->validate('test.png', new ValidFileFilter());
|
||||||
|
$this->assertCount(1, $violations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,52 +24,54 @@ namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
use App\Validator\Constraints\ValidGTIN;
|
use App\Validator\Constraints\ValidGTIN;
|
||||||
use App\Validator\Constraints\ValidGTINValidator;
|
use App\Validator\Constraints\ValidGTINValidator;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
final class ValidGTINValidatorTest extends ConstraintValidatorTestCase
|
final class ValidGTINValidatorTest extends ConstraintValidatorTestCase
|
||||||
{
|
{
|
||||||
|
|
||||||
public function testAllowNull(): void
|
|
||||||
{
|
|
||||||
$this->validator->validate(null, new ValidGTIN());
|
|
||||||
$this->assertNoViolation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testValidGTIN8(): void
|
|
||||||
{
|
|
||||||
$this->validator->validate('12345670', new ValidGTIN());
|
|
||||||
$this->assertNoViolation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testValidGTIN12(): void
|
|
||||||
{
|
|
||||||
$this->validator->validate('123456789012', new ValidGTIN());
|
|
||||||
$this->assertNoViolation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testValidGTIN13(): void
|
|
||||||
{
|
|
||||||
$this->validator->validate('1234567890128', new ValidGTIN());
|
|
||||||
$this->assertNoViolation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testValidGTIN14(): void
|
|
||||||
{
|
|
||||||
$this->validator->validate('12345678901231', new ValidGTIN());
|
|
||||||
$this->assertNoViolation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testInvalidGTIN(): void
|
|
||||||
{
|
|
||||||
$this->validator->validate('1234567890123', new ValidGTIN());
|
|
||||||
$this->buildViolation('validator.invalid_gtin')
|
|
||||||
->assertRaised();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function createValidator(): ConstraintValidatorInterface
|
protected function createValidator(): ConstraintValidatorInterface
|
||||||
{
|
{
|
||||||
return new ValidGTINValidator();
|
return new ValidGTINValidator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- values that must produce no violation ---
|
||||||
|
|
||||||
|
public static function validValuesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'null is skipped' => [null];
|
||||||
|
yield 'empty string is skipped' => [''];
|
||||||
|
yield 'valid GTIN-8' => ['12345670'];
|
||||||
|
yield 'valid GTIN-12' => ['123456789012'];
|
||||||
|
yield 'valid GTIN-13' => ['1234567890128'];
|
||||||
|
yield 'valid GTIN-14' => ['12345678901231'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('validValuesProvider')]
|
||||||
|
public function testValidValue(mixed $value): void
|
||||||
|
{
|
||||||
|
$this->validator->validate($value, new ValidGTIN());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- values that must produce a violation ---
|
||||||
|
|
||||||
|
public static function invalidValuesProvider(): \Generator
|
||||||
|
{
|
||||||
|
yield 'wrong check digit (GTIN-13)' => ['1234567890123'];
|
||||||
|
yield 'non-numeric string' => ['ABCDEFGHIJKLM'];
|
||||||
|
yield 'wrong length — 9 digits' => ['123456789'];
|
||||||
|
yield 'wrong length — 11 digits' => ['12345678901'];
|
||||||
|
yield 'leading whitespace' => [' 1234567890128'];
|
||||||
|
yield 'trailing whitespace' => ['1234567890128 '];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('invalidValuesProvider')]
|
||||||
|
public function testInvalidValue(string $value): void
|
||||||
|
{
|
||||||
|
$this->validator->validate($value, new ValidGTIN());
|
||||||
|
$this->buildViolation('validator.invalid_gtin')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
123
tests/Validator/Constraints/ValidPartLotValidatorTest.php
Normal file
123
tests/Validator/Constraints/ValidPartLotValidatorTest.php
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Entity\Parts\PartLot;
|
||||||
|
use App\Entity\Parts\StorageLocation;
|
||||||
|
use App\Validator\Constraints\ValidPartLot;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
|
final class ValidPartLotValidatorTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private static ValidatorInterface $validator;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
self::$validator = self::getContainer()->get('validator');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPartLotWithoutStorageLocationIsValid(): void
|
||||||
|
{
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setPart(new Part());
|
||||||
|
// No storage location set → validation should pass without any location checks
|
||||||
|
|
||||||
|
$violations = self::$validator->validate($lot, new ValidPartLot());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPartLotWithNonFullNonRestrictedStorageLocationIsValid(): void
|
||||||
|
{
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setPart(new Part());
|
||||||
|
|
||||||
|
$location = new StorageLocation();
|
||||||
|
// Default: not full, not limited — should be valid
|
||||||
|
$lot->setStorageLocation($location);
|
||||||
|
|
||||||
|
$violations = self::$validator->validate($lot, new ValidPartLot());
|
||||||
|
$this->assertCount(0, $violations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPartLotWithFullLocationAndNewLotRaisesViolation(): void
|
||||||
|
{
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setPart(new Part());
|
||||||
|
|
||||||
|
$location = new StorageLocation();
|
||||||
|
$location->setIsFull(true);
|
||||||
|
$lot->setStorageLocation($location);
|
||||||
|
// The lot has no ID (new entity), so "parts" is empty, and a full location will reject it
|
||||||
|
|
||||||
|
$violations = self::$validator->validate($lot, new ValidPartLot());
|
||||||
|
// Should raise a violation because the location is full and the part is not in the existing parts list
|
||||||
|
$this->assertGreaterThan(0, count($violations));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNonPartLotValueThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->expectException(\Symfony\Component\Form\Exception\UnexpectedTypeException::class);
|
||||||
|
self::$validator->validate('not a part lot', new ValidPartLot());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPartLotWithFullLocationRaisesNamedViolation(): void
|
||||||
|
{
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setPart(new Part());
|
||||||
|
|
||||||
|
$location = new StorageLocation();
|
||||||
|
$location->setIsFull(true);
|
||||||
|
$lot->setStorageLocation($location);
|
||||||
|
|
||||||
|
$violations = self::$validator->validate($lot, new ValidPartLot());
|
||||||
|
// Expect exactly one violation on the storage_location path
|
||||||
|
$this->assertCount(1, $violations);
|
||||||
|
$this->assertSame('storage_location', $violations[0]->getPropertyPath());
|
||||||
|
$this->assertStringContainsString('location_full', $violations[0]->getMessageTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLimitToExistingPartsWithNewLotRaisesViolation(): void
|
||||||
|
{
|
||||||
|
$lot = new PartLot();
|
||||||
|
$lot->setPart(new Part());
|
||||||
|
|
||||||
|
$location = new StorageLocation();
|
||||||
|
$location->setLimitToExistingParts(true);
|
||||||
|
$lot->setStorageLocation($location);
|
||||||
|
|
||||||
|
// New lot (no ID) → parts collection is empty → part is not in the list → violation
|
||||||
|
$violations = self::$validator->validate($lot, new ValidPartLot());
|
||||||
|
$this->assertCount(1, $violations);
|
||||||
|
$this->assertSame('storage_location', $violations[0]->getPropertyPath());
|
||||||
|
$this->assertSame('validator.part_lot.only_existing', $violations[0]->getMessageTemplate());
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: The 'location_full.no_increase' violation (raised when a lot's amount
|
||||||
|
// is increased while its storage location is marked full) requires the entity to
|
||||||
|
// carry a real Doctrine originalEntityData snapshot, which is only set after an
|
||||||
|
// actual persist+flush. Testing that path belongs in a database integration test.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Validator\Constraints\Year2038BugWorkaround;
|
||||||
|
use App\Validator\Constraints\Year2038BugWorkaroundValidator;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
final class Year2038BugWorkaroundValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator(): ConstraintValidatorInterface
|
||||||
|
{
|
||||||
|
// Disable validation by default so tests run on both 32- and 64-bit systems
|
||||||
|
return new Year2038BugWorkaroundValidator(disable_validation: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsNotActivatedWhenDisabled(): void
|
||||||
|
{
|
||||||
|
$validator = new Year2038BugWorkaroundValidator(disable_validation: true);
|
||||||
|
$this->assertFalse($validator->isActivated());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsNotActivatedOn64Bit(): void
|
||||||
|
{
|
||||||
|
// On any normal 64-bit CI/dev system PHP_INT_SIZE === 8, so activation requires 32-bit
|
||||||
|
if (PHP_INT_SIZE !== 8) {
|
||||||
|
$this->markTestSkipped('This test is only meaningful on 64-bit systems.');
|
||||||
|
}
|
||||||
|
$validator = new Year2038BugWorkaroundValidator(disable_validation: false);
|
||||||
|
$this->assertFalse($validator->isActivated());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullValueProducesNoViolation(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(null, new Year2038BugWorkaround());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDateBefore2038ProducesNoViolationWhenDisabled(): void
|
||||||
|
{
|
||||||
|
$this->validator->validate(new \DateTime('2037-01-01'), new Year2038BugWorkaround());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDateAfter2038ProducesNoViolationWhenDisabled(): void
|
||||||
|
{
|
||||||
|
// Validation disabled → even a "bad" date causes no violation
|
||||||
|
$this->validator->validate(new \DateTime('2039-01-01'), new Year2038BugWorkaround());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
160
yarn.lock
160
yarn.lock
|
|
@ -529,10 +529,10 @@
|
||||||
"@babel/helper-module-transforms" "^7.28.6"
|
"@babel/helper-module-transforms" "^7.28.6"
|
||||||
"@babel/helper-plugin-utils" "^7.28.6"
|
"@babel/helper-plugin-utils" "^7.28.6"
|
||||||
|
|
||||||
"@babel/plugin-transform-modules-systemjs@^7.29.0":
|
"@babel/plugin-transform-modules-systemjs@^7.29.4":
|
||||||
version "7.29.0"
|
version "7.29.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20"
|
||||||
integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==
|
integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-module-transforms" "^7.28.6"
|
"@babel/helper-module-transforms" "^7.28.6"
|
||||||
"@babel/helper-plugin-utils" "^7.28.6"
|
"@babel/helper-plugin-utils" "^7.28.6"
|
||||||
|
|
@ -731,9 +731,9 @@
|
||||||
"@babel/helper-plugin-utils" "^7.28.6"
|
"@babel/helper-plugin-utils" "^7.28.6"
|
||||||
|
|
||||||
"@babel/preset-env@^7.19.4":
|
"@babel/preset-env@^7.19.4":
|
||||||
version "7.29.3"
|
version "7.29.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.3.tgz#2bbd5b0162e6a762adfe356f4aecdef837a3d574"
|
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.5.tgz#c48b7ed94582c8b685e21b8b42de8633ec289268"
|
||||||
integrity sha512-ySZypNLAIH1ClygLDQzVMoGQRViATnkHkYYV6TcNDz+8+jwZCdsguGvsb3EY5d9wyWyhmF1iSuFM0Yh5XPnqSA==
|
integrity sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/compat-data" "^7.29.3"
|
"@babel/compat-data" "^7.29.3"
|
||||||
"@babel/helper-compilation-targets" "^7.28.6"
|
"@babel/helper-compilation-targets" "^7.28.6"
|
||||||
|
|
@ -774,7 +774,7 @@
|
||||||
"@babel/plugin-transform-member-expression-literals" "^7.27.1"
|
"@babel/plugin-transform-member-expression-literals" "^7.27.1"
|
||||||
"@babel/plugin-transform-modules-amd" "^7.27.1"
|
"@babel/plugin-transform-modules-amd" "^7.27.1"
|
||||||
"@babel/plugin-transform-modules-commonjs" "^7.28.6"
|
"@babel/plugin-transform-modules-commonjs" "^7.28.6"
|
||||||
"@babel/plugin-transform-modules-systemjs" "^7.29.0"
|
"@babel/plugin-transform-modules-systemjs" "^7.29.4"
|
||||||
"@babel/plugin-transform-modules-umd" "^7.27.1"
|
"@babel/plugin-transform-modules-umd" "^7.27.1"
|
||||||
"@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0"
|
"@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0"
|
||||||
"@babel/plugin-transform-new-target" "^7.27.1"
|
"@babel/plugin-transform-new-target" "^7.27.1"
|
||||||
|
|
@ -1644,28 +1644,28 @@
|
||||||
resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.7.tgz#42a5ea40ce1bfe6cffbc1b811dc4e32dd8d0273a"
|
resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.7.tgz#42a5ea40ce1bfe6cffbc1b811dc4e32dd8d0273a"
|
||||||
integrity sha512-AvEdkQNkNvh9+yGGHto8ABBsicEzFjLtSSbl61c9D0yq+RrIsrwTpz/H3RmDhvdtdteywQRItVuS18XOc+0p2A==
|
integrity sha512-AvEdkQNkNvh9+yGGHto8ABBsicEzFjLtSSbl61c9D0yq+RrIsrwTpz/H3RmDhvdtdteywQRItVuS18XOc+0p2A==
|
||||||
|
|
||||||
"@jest/pattern@30.0.1":
|
"@jest/pattern@30.4.0":
|
||||||
version "30.0.1"
|
version "30.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f"
|
resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.4.0.tgz#fcb519eeacc25caa3768f787595a27afa15302ae"
|
||||||
integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==
|
integrity sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
jest-regex-util "30.0.1"
|
jest-regex-util "30.4.0"
|
||||||
|
|
||||||
"@jest/schemas@30.0.5":
|
"@jest/schemas@30.4.1":
|
||||||
version "30.0.5"
|
version "30.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473"
|
resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.4.1.tgz#c3703fdd71357e2c83aa59bd38469e60a11529c6"
|
||||||
integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==
|
integrity sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sinclair/typebox" "^0.34.0"
|
"@sinclair/typebox" "^0.34.0"
|
||||||
|
|
||||||
"@jest/types@30.3.0":
|
"@jest/types@30.4.1":
|
||||||
version "30.3.0"
|
version "30.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.3.0.tgz#cada800d323cb74945c24ac74615fdb312a6c85f"
|
resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.4.1.tgz#f79b647a85cb2ff4a90cc55984b31dae820db1f7"
|
||||||
integrity sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==
|
integrity sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jest/pattern" "30.0.1"
|
"@jest/pattern" "30.4.0"
|
||||||
"@jest/schemas" "30.0.5"
|
"@jest/schemas" "30.4.1"
|
||||||
"@types/istanbul-lib-coverage" "^2.0.6"
|
"@types/istanbul-lib-coverage" "^2.0.6"
|
||||||
"@types/istanbul-reports" "^3.0.4"
|
"@types/istanbul-reports" "^3.0.4"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
@ -1854,9 +1854,9 @@
|
||||||
"@types/json-schema" "*"
|
"@types/json-schema" "*"
|
||||||
|
|
||||||
"@types/estree@*", "@types/estree@^1.0.8":
|
"@types/estree@*", "@types/estree@^1.0.8":
|
||||||
version "1.0.8"
|
version "1.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24"
|
||||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==
|
||||||
|
|
||||||
"@types/hast@3.0.4", "@types/hast@^3.0.0":
|
"@types/hast@3.0.4", "@types/hast@^3.0.0":
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
|
|
@ -1902,9 +1902,9 @@
|
||||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "25.6.0"
|
version "25.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.2.tgz#8c491201373690e4ef2a2ffed0dfb510a5830b92"
|
||||||
integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==
|
integrity sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~7.19.0"
|
undici-types "~7.19.0"
|
||||||
|
|
||||||
|
|
@ -1936,9 +1936,9 @@
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.3.0":
|
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.3.0":
|
||||||
version "1.3.0"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
|
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz#0e8f34854df7966b09304a18e808b23997bb9fc1"
|
||||||
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
|
integrity sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==
|
||||||
|
|
||||||
"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
|
"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
|
|
@ -2238,9 +2238,9 @@ base64-js@^1.1.2, base64-js@^1.3.0:
|
||||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||||
|
|
||||||
baseline-browser-mapping@^2.10.12:
|
baseline-browser-mapping@^2.10.12:
|
||||||
version "2.10.27"
|
version "2.10.29"
|
||||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz#fee941c2a0b42cdf83c6427e4c830b1d0bdab2c3"
|
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz#47bdc13027af28d341f367a4f35a07ce872e27b4"
|
||||||
integrity sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==
|
integrity sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==
|
||||||
|
|
||||||
big.js@^5.2.2:
|
big.js@^5.2.2:
|
||||||
version "5.2.2"
|
version "5.2.2"
|
||||||
|
|
@ -2325,9 +2325,9 @@ caniuse-api@^3.0.0:
|
||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001782:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001782:
|
||||||
version "1.0.30001791"
|
version "1.0.30001792"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz#dfb93d85c40ad380c57123e72e10f3c575786b51"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz#ca8bb9be244835a335e2018272ce7223691873c5"
|
||||||
integrity sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==
|
integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==
|
||||||
|
|
||||||
ccount@^2.0.0:
|
ccount@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
|
@ -2917,9 +2917,9 @@ domutils@^3.0.1:
|
||||||
domhandler "^5.0.3"
|
domhandler "^5.0.3"
|
||||||
|
|
||||||
electron-to-chromium@^1.5.328:
|
electron-to-chromium@^1.5.328:
|
||||||
version "1.5.349"
|
version "1.5.353"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz#9b9c6a6d84d1107557c18a9336099ce0ee890e5b"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz#01e8a8e25a0bf13e631106045f177d0568ca91c2"
|
||||||
integrity sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==
|
integrity sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
|
|
@ -2932,9 +2932,9 @@ emojis-list@^3.0.0:
|
||||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||||
|
|
||||||
enhanced-resolve@^5.0.0, enhanced-resolve@^5.20.0:
|
enhanced-resolve@^5.0.0, enhanced-resolve@^5.20.0:
|
||||||
version "5.21.0"
|
version "5.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz#bb8e6fabaf74930de70e61397798750429e5b1ae"
|
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz#fa7fed23679e9169dfb705b8e201924421c4414a"
|
||||||
integrity sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==
|
integrity sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs "^4.2.4"
|
graceful-fs "^4.2.4"
|
||||||
tapable "^2.3.3"
|
tapable "^2.3.3"
|
||||||
|
|
@ -3039,9 +3039,9 @@ fast-deep-equal@^3.1.3:
|
||||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||||
|
|
||||||
fast-uri@^3.0.1:
|
fast-uri@^3.0.1:
|
||||||
version "3.1.1"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.1.tgz#dd085fec2494a2a33bac6e61277374669e1dd774"
|
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
|
||||||
integrity sha512-h2r7rcm6Ee/J8o0LD5djLuFVcfbZxhvho4vvsbeV0aMvXjUgqv4YpxpkEx0d68l6+IleVfLAdVEfhR7QNMkGHQ==
|
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
|
||||||
|
|
||||||
fastest-levenshtein@1.0.16, fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16:
|
fastest-levenshtein@1.0.16, fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16:
|
||||||
version "1.0.16"
|
version "1.0.16"
|
||||||
|
|
@ -3138,7 +3138,7 @@ has-flag@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||||
|
|
||||||
hasown@^2.0.2:
|
hasown@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c"
|
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.3.tgz#5e5c2b15b60370a4c7930c383dfb76bf17bc403c"
|
||||||
integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==
|
integrity sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==
|
||||||
|
|
@ -3350,11 +3350,11 @@ intl-messageformat@^10.5.11:
|
||||||
tslib "^2.8.0"
|
tslib "^2.8.0"
|
||||||
|
|
||||||
is-core-module@^2.16.1:
|
is-core-module@^2.16.1:
|
||||||
version "2.16.1"
|
version "2.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
|
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082"
|
||||||
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
|
integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==
|
||||||
dependencies:
|
dependencies:
|
||||||
hasown "^2.0.2"
|
hasown "^2.0.3"
|
||||||
|
|
||||||
is-docker@^2.0.0:
|
is-docker@^2.0.0:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
|
|
@ -3405,17 +3405,17 @@ isobject@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||||
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
|
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
|
||||||
|
|
||||||
jest-regex-util@30.0.1:
|
jest-regex-util@30.4.0:
|
||||||
version "30.0.1"
|
version "30.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b"
|
resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.4.0.tgz#f75ccc43857633df2563a03588b5cb45c7c2941b"
|
||||||
integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==
|
integrity sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==
|
||||||
|
|
||||||
jest-util@30.3.0:
|
jest-util@30.4.1:
|
||||||
version "30.3.0"
|
version "30.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.3.0.tgz#95a4fbacf2dac20e768e2f1744b70519f2ba7980"
|
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.4.1.tgz#979c9d014fdd12bb95d3dcde0192e1a9e0bc93d6"
|
||||||
integrity sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==
|
integrity sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jest/types" "30.3.0"
|
"@jest/types" "30.4.1"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
chalk "^4.1.2"
|
chalk "^4.1.2"
|
||||||
ci-info "^4.2.0"
|
ci-info "^4.2.0"
|
||||||
|
|
@ -3432,13 +3432,13 @@ jest-worker@^27.4.5:
|
||||||
supports-color "^8.0.0"
|
supports-color "^8.0.0"
|
||||||
|
|
||||||
jest-worker@^30.0.5:
|
jest-worker@^30.0.5:
|
||||||
version "30.3.0"
|
version "30.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.3.0.tgz#ae4dc1f1d93d0cba1415624fcedaec40ea764f14"
|
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.4.1.tgz#ac010eb6c512425748a39e2d6bf05b2c4866ca4f"
|
||||||
integrity sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==
|
integrity sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
"@ungap/structured-clone" "^1.3.0"
|
"@ungap/structured-clone" "^1.3.0"
|
||||||
jest-util "30.3.0"
|
jest-util "30.4.1"
|
||||||
merge-stream "^2.0.0"
|
merge-stream "^2.0.0"
|
||||||
supports-color "^8.1.1"
|
supports-color "^8.1.1"
|
||||||
|
|
||||||
|
|
@ -4181,9 +4181,9 @@ pdfkit@^0.18.0:
|
||||||
png-js "^1.0.0"
|
png-js "^1.0.0"
|
||||||
|
|
||||||
pdfmake@^0.3.7:
|
pdfmake@^0.3.7:
|
||||||
version "0.3.7"
|
version "0.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.3.7.tgz#7db4f5d83306d344cda20afdd59cd09cf4acdae1"
|
resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.3.8.tgz#cebff884636fddda02af04599530355aa855131f"
|
||||||
integrity sha512-SwTFcaH3kCJBlPFWi/YB34zRg6lpCxq90tkZ9GxfSi9/v4Tk96cv4IvOstA+CC40rdW1OzQIuNhD2DLD1RDVgA==
|
integrity sha512-ywj3MESfqOW7sOjXZiBKaWk7XLncZ9caMflM3WSbc0Do8Wpwn9DBV8ceKZqkz1M/avl8i+ccS2f8THZRyFaCGQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
linebreak "^1.1.0"
|
linebreak "^1.1.0"
|
||||||
pdfkit "^0.18.0"
|
pdfkit "^0.18.0"
|
||||||
|
|
@ -4748,9 +4748,9 @@ semver@^6.3.1:
|
||||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
semver@^7.3.2, semver@^7.3.4, semver@^7.6.3:
|
semver@^7.3.2, semver@^7.3.4, semver@^7.6.3:
|
||||||
version "7.7.4"
|
version "7.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.0.tgz#ed0661039fcbcda2ce71f01fa6adbefaa77040df"
|
||||||
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
|
integrity sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==
|
||||||
|
|
||||||
serialize-javascript@^6.0.2:
|
serialize-javascript@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
|
|
@ -4932,9 +4932,9 @@ tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0, tapable@^2.3.3:
|
||||||
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
|
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
|
||||||
|
|
||||||
terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.17:
|
terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.17:
|
||||||
version "5.5.0"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz#d92b8e2c892dd09c683c38120394267e8d8660ef"
|
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.6.0.tgz#8e7caad248183ab9e91ff08a83b0fc9f0439c3c3"
|
||||||
integrity sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==
|
integrity sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/trace-mapping" "^0.3.25"
|
"@jridgewell/trace-mapping" "^0.3.25"
|
||||||
jest-worker "^27.4.5"
|
jest-worker "^27.4.5"
|
||||||
|
|
@ -4942,9 +4942,9 @@ terser-webpack-plugin@^5.3.0, terser-webpack-plugin@^5.3.17:
|
||||||
terser "^5.31.1"
|
terser "^5.31.1"
|
||||||
|
|
||||||
terser@^5.31.1:
|
terser@^5.31.1:
|
||||||
version "5.46.2"
|
version "5.47.1"
|
||||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.2.tgz#b9529672d5b0024c7959571c83b82f65077b2a4f"
|
resolved "https://registry.yarnpkg.com/terser/-/terser-5.47.1.tgz#99b298e51bc41214304847de1429ec92fd1f7648"
|
||||||
integrity sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==
|
integrity sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/source-map" "^0.3.3"
|
"@jridgewell/source-map" "^0.3.3"
|
||||||
acorn "^8.15.0"
|
acorn "^8.15.0"
|
||||||
|
|
@ -4974,9 +4974,9 @@ to-regex-range@^5.0.1:
|
||||||
is-number "^7.0.0"
|
is-number "^7.0.0"
|
||||||
|
|
||||||
tom-select@^2.1.0:
|
tom-select@^2.1.0:
|
||||||
version "2.6.0"
|
version "2.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.6.0.tgz#8582363389dd17157ed11692320530bcd4111fbf"
|
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.6.1.tgz#54be5c4431d5d59c8c4897e6e051963bac11f44a"
|
||||||
integrity sha512-o2ToBjhUAnrrQvW/hrY9c//TpOpAKYSlfuFnf0DIwNy+ua+mmYnsF4PxN/PpzBfUIfEFkNYAngeGBfOAZWF3tw==
|
integrity sha512-d/1kngVOQTGcI/2pVDfDLYjtjUgSSd3fSgkYUpi0y+yRtQQu2kzljj3aUdqMfqc45cjPvDEpfDt/hSX4awDFTg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@orchidjs/sifter" "^1.1.0"
|
"@orchidjs/sifter" "^1.1.0"
|
||||||
"@orchidjs/unicode-variants" "^1.1.2"
|
"@orchidjs/unicode-variants" "^1.1.2"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue