- {% if dto.preview_image_url %}
-
- {% endif %}
+
- {# Check for matches against source keyword (what was searched) #}
- {% set sourceKw = result.sourceKeyword|default('')|lower %}
- {% set nameMatch = sourceKw is not empty and dto.name is not null and dto.name|lower == sourceKw %}
- {% set mpnMatch = sourceKw is not empty and dto.mpn is not null and dto.mpn|lower == sourceKw %}
- {% set spnMatch = sourceKw is not empty and dto.provider_id is not null and dto.provider_id|lower == sourceKw %}
- {% set anyMatch = nameMatch or mpnMatch or spnMatch %}
{% if dto.provider_url is not null %}
- {{ dto.name }}
+ {{ dto.name }}
{% else %}
- {{ dto.name }}
- {% endif %}
- {% if nameMatch %}
-
+ {{ dto.name }}
{% endif %}
{% if dto.mpn is not null %}
- {{ dto.mpn }}
- {% if mpnMatch %}
- MPN
- {% endif %}
+ {{ dto.mpn }}
{% endif %}
{{ dto.description }}
{{ dto.manufacturer ?? '' }}
{{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
- {{ dto.provider_id }}
- {% if spnMatch %}
- SPN
- {% endif %}
+ {{ dto.provider_id }}
- {% if anyMatch %}
- {% trans %}info_providers.bulk_import.match{% endtrans %}
- {% else %}
- {{ result.sourceField ?? 'unknown' }}
- {% endif %}
+ {{ result.sourceField ?? 'unknown' }}
{% if result.sourceKeyword %}
- {{ result.sourceKeyword }}
- {% endif %}
+ {{ result.sourceKeyword }}
+ {% endif %}
-
-
-
-
- {{ form_row(form.no_cache_search) }}
- {{ form_row(form.no_cache_details) }}
-
-
-
{{ form_row(form.submit) }}
{{ form_end(form) }}
@@ -129,16 +116,16 @@
{% if update_target %} {# We update an existing part #}
{% set href = path('info_providers_update_part',
- {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'id': update_target.iD, 'no_cache': no_cache_details ? 1 : null}) %}
+ {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'id': update_target.iD}) %}
{% else %} {# Create a fresh part #}
{% set href = path('info_providers_create_part',
- {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'no_cache': no_cache_details ? 1 : null}) %}
+ {'providerKey': dto.provider_key, 'providerId': dto.provider_id}) %}
{% endif %}
{# If we have no local part, then we can just show the create button #}
{% if localPart is null %}
+ target="_blank" title="{% trans %}part.create.btn{% endtrans %}">
{% else %} {# Otherwise add a button group with all three buttons #}
@@ -152,7 +139,7 @@
target="_blank" title="{% trans %}info_providers.search.show_existing_part{% endtrans %}">
-
diff --git a/templates/parts/info/_add_lot_modal.html.twig b/templates/parts/info/_add_lot_modal.html.twig
deleted file mode 100644
index b31f368f..00000000
--- a/templates/parts/info/_add_lot_modal.html.twig
+++ /dev/null
@@ -1,46 +0,0 @@
-{% if add_lot_form is not null %}
-{% form_theme add_lot_form 'form/extended_bootstrap_layout.html.twig' %}
-
-
-
-
- {{ form_start(add_lot_form) }}
-
-
- {{ form_row(add_lot_form.description) }}
- {{ form_row(add_lot_form.storage_location) }}
- {{ form_row(add_lot_form.amount) }}
- {{ form_row(add_lot_form.instock_unknown) }}
- {{ form_row(add_lot_form.needs_refill) }}
- {{ form_row(add_lot_form.expiration_date) }}
-
-
-
-
- {{ form_end(add_lot_form) }}
-
-
-
-{% endif %}
diff --git a/templates/parts/info/_part_lots.html.twig b/templates/parts/info/_part_lots.html.twig
index 7e53aec1..70e5dc4e 100644
--- a/templates/parts/info/_part_lots.html.twig
+++ b/templates/parts/info/_part_lots.html.twig
@@ -3,7 +3,6 @@
{% include "parts/info/_withdraw_modal.html.twig" %}
{% include "parts/info/_stocktake_modal.html.twig" %}
-{% include "parts/info/_add_lot_modal.html.twig" %}
-
-{% if add_lot_form is not null %}
-
-
- {% trans %}part_lot.create{% endtrans %}
-
-{% endif %}
diff --git a/templates/projects/info/_info.html.twig b/templates/projects/info/_info.html.twig
index c3a8e86d..b95be253 100644
--- a/templates/projects/info/_info.html.twig
+++ b/templates/projects/info/_info.html.twig
@@ -55,32 +55,6 @@
- {% set n = number_of_builds ?? 1 %}
- {% set total_build_price = buildHelper.roundedTotalBuildPrice(project, n, app.user.currency ?? null) %}
- {% set unit_build_price = buildHelper.roundedUnitBuildPrice(project, n, app.user.currency ?? null) %}
- {% if total_build_price is not null %}
-
-
-
-
- {% trans %}project.info.total_build_price{% endtrans %}:
- {{ total_build_price | format_money(app.user.currency ?? null, 2) }}
- {% if n > 1 and unit_build_price is not null %}
-
- ({% trans %}project.info.per_unit_price{% endtrans %}: {{ unit_build_price | format_money(app.user.currency ?? null, 2) }})
-
- {% endif %}
-
-
-
- {% endif %}
-
{% if project.children is not empty %}
@@ -95,9 +69,9 @@
{% if project.comment is not empty %}
-
-
{% trans %}comment.label{% endtrans %}:
- {{ project.comment|format_markdown }}
-
+
+
{% trans %}comment.label{% endtrans %}:
+ {{ project.comment|format_markdown }}
+
{% endif %}
-
+
\ No newline at end of file
diff --git a/templates/settings/kicad_list_editor.html.twig b/templates/settings/kicad_list_editor.html.twig
deleted file mode 100644
index 33ff00ec..00000000
--- a/templates/settings/kicad_list_editor.html.twig
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "main_card.html.twig" %}
-
-{% block title %}{% trans %}settings.misc.kicad_eda.editor.title{% endtrans %}{% endblock %}
-
-{% block card_title %} {% trans %}settings.misc.kicad_eda.editor.title{% endtrans %}{% endblock %}
-
-{% block card_content %}
-
- {% trans %}settings.misc.kicad_eda.editor.description{% endtrans %}
-
-
- {{ form_start(form) }}
- {{ form_row(form.useCustomList) }}
-
-
-
- {{ form_row(form.customFootprints) }}
- {{ form_row(form.customSymbols) }}
-
-
- {{ form_row(form.defaultFootprints) }}
- {{ form_row(form.defaultSymbols) }}
-
-
-
- {{ form_row(form.save) }}
- {{ form_end(form) }}
-{% endblock %}
diff --git a/templates/settings/settings.html.twig b/templates/settings/settings.html.twig
index 325118d6..a2c01085 100644
--- a/templates/settings/settings.html.twig
+++ b/templates/settings/settings.html.twig
@@ -49,15 +49,6 @@
{{ form_widget(section_widget) }}
- {% if section_widget.vars.name == 'kicadEDA' %}
-
- {% endif %}
{% if not loop.last %}
diff --git a/tests/ApplicationAvailabilityFunctionalTest.php b/tests/ApplicationAvailabilityFunctionalTest.php
index 3bb222d0..c7449411 100644
--- a/tests/ApplicationAvailabilityFunctionalTest.php
+++ b/tests/ApplicationAvailabilityFunctionalTest.php
@@ -60,7 +60,6 @@ final class ApplicationAvailabilityFunctionalTest extends WebTestCase
//User related things
yield ['/user/settings'];
yield ['/user/info'];
- yield ['/settings/misc/kicad-lists'];
//Login/logout
yield ['/login'];
diff --git a/tests/Controller/AuthorizationTest.php b/tests/Controller/AuthorizationTest.php
deleted file mode 100644
index 4e211301..00000000
--- a/tests/Controller/AuthorizationTest.php
+++ /dev/null
@@ -1,222 +0,0 @@
-.
- */
-
-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();
- }
-}
diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php
index d768f55c..ec3629fe 100644
--- a/tests/Controller/BulkInfoProviderImportControllerTest.php
+++ b/tests/Controller/BulkInfoProviderImportControllerTest.php
@@ -589,296 +589,6 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
return $parts;
}
- public function testQuickApplyWithNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/quick-apply');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertArrayHasKey('error', $response);
- }
-
- public function testQuickApplyWithNonExistentPart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/999999/quick-apply');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testQuickApplyWithNoSearchResults(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- // Empty search results - no provider results for any parts
- $job->setSearchResults(new BulkSearchResponseDTO([
- new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [], errors: [])
- ]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- // Quick apply without providing providerKey/providerId and no search results available
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/quick-apply', [], [], [
- 'CONTENT_TYPE' => 'application/json',
- ], json_encode([]));
-
- $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertFalse($response['success']);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testQuickApplyAccessControl(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $admin = $userRepository->findOneBy(['name' => 'admin']);
- $readonly = $userRepository->findOneBy(['name' => 'noread']);
-
- if (!$admin || !$readonly) {
- $this->markTestSkipped('Required test users not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1]);
-
- // Create job owned by readonly user
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($readonly);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- // Admin tries to quick apply on readonly user's job - should fail
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/quick-apply');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- // Clean up
- $jobId = $job->getId();
- $entityManager->clear();
- $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($persistedJob) {
- $entityManager->remove($persistedJob);
- $entityManager->flush();
- }
- }
-
- public function testQuickApplyAllWithNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/quick-apply-all');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertArrayHasKey('error', $response);
- }
-
- public function testQuickApplyAllWithNoResults(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1, 2]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- // Empty search results for all parts
- $job->setSearchResults(new BulkSearchResponseDTO([
- new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [], errors: []),
- new BulkSearchPartResultsDTO(part: $parts[1], searchResults: [], errors: []),
- ]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/quick-apply-all');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertEquals(0, $response['applied']);
- $this->assertEquals(2, $response['no_results']);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testQuickApplyAllAccessControl(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $readonly = $userRepository->findOneBy(['name' => 'noread']);
-
- if (!$readonly) {
- $this->markTestSkipped('Required test users not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($readonly);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- // Admin tries quick apply all on readonly user's job
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/quick-apply-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- // Clean up
- $jobId = $job->getId();
- $entityManager->clear();
- $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($persistedJob) {
- $entityManager->remove($persistedJob);
- $entityManager->flush();
- }
- }
-
- public function testStep2TemplateRenderingWithQuickApplyButtons(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = static::getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- $job->addPart($part);
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
-
- $searchResults = new BulkSearchResponseDTO(partResults: [
- new BulkSearchPartResultsDTO(part: $part,
- searchResults: [new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(provider_key: 'test_provider', provider_id: 'TEST123', name: 'Test Component', description: 'Test description', manufacturer: 'Test Mfg', mpn: 'TEST-MPN', provider_url: 'https://example.com/test', preview_image_url: null),
- sourceField: 'mpn',
- sourceKeyword: 'TEST-MPN',
- )]
- )
- ]);
-
- $job->setSearchResults($searchResults);
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('GET', '/tools/bulk_info_provider_import/step2/' . $job->getId());
-
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- self::assertResponseStatusCodeSame(Response::HTTP_OK);
-
- $content = (string) $client->getResponse()->getContent();
- // Verify quick apply buttons are rendered (Stimulus renders camelCase as kebab-case data attributes)
- $this->assertStringContainsString('quick-apply-url-value', $content);
- $this->assertStringContainsString('quick-apply-all-url-value', $content);
-
- // Clean up
- $jobId = $job->getId();
- $entityManager->clear();
- $jobToRemove = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($jobToRemove) {
- $entityManager->remove($jobToRemove);
- $entityManager->flush();
- }
- }
-
public function testStep1Form(): void
{
$client = static::createClient();
@@ -1025,9 +735,13 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
new BulkSearchFieldMappingDTO('test_supplier_spn', ['test'], 2)
];
- // The service should return an empty response DTO when no results are found
- $response = $bulkService->performBulkSearch([$part], $fieldMappings, false);
- $this->assertFalse($response->hasAnyResults());
+ // The service should be able to process the request and throw an exception when no results are found
+ try {
+ $bulkService->performBulkSearch([$part], $fieldMappings, false);
+ $this->fail('Expected RuntimeException to be thrown when no search results are found');
+ } catch (\RuntimeException $e) {
+ $this->assertStringContainsString('No search results found', $e->getMessage());
+ }
}
public function testBulkInfoProviderServiceBatchProcessing(): void
@@ -1051,9 +765,13 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
new BulkSearchFieldMappingDTO('empty', ['test'], 1)
];
- // The service should return an empty response DTO when no results are found
- $response = $bulkService->performBulkSearch([$part], $fieldMappings, false);
- $this->assertFalse($response->hasAnyResults());
+ // The service should be able to process the request and throw an exception when no results are found
+ try {
+ $response = $bulkService->performBulkSearch([$part], $fieldMappings, false);
+ $this->fail('Expected RuntimeException to be thrown when no search results are found');
+ } catch (\RuntimeException $e) {
+ $this->assertStringContainsString('No search results found', $e->getMessage());
+ }
}
public function testBulkInfoProviderServicePrefetchDetails(): void
@@ -1169,684 +887,4 @@ final class BulkInfoProviderImportControllerTest extends WebTestCase
$entityManager->remove($job);
$entityManager->flush();
}
-
- /**
- * Helper to create a job with search results for testing.
- */
- private function createJobWithSearchResults(object $entityManager, object $user, array $parts, string $status = 'in_progress'): BulkInfoProviderImportJob
- {
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
-
- $statusEnum = match ($status) {
- 'pending' => BulkImportJobStatus::PENDING,
- 'completed' => BulkImportJobStatus::COMPLETED,
- 'stopped' => BulkImportJobStatus::STOPPED,
- default => BulkImportJobStatus::IN_PROGRESS,
- };
- $job->setStatus($statusEnum);
-
- // Create search results with a result per part
- $partResults = [];
- foreach ($parts as $part) {
- $partResults[] = new BulkSearchPartResultsDTO(
- part: $part,
- searchResults: [
- new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(
- provider_key: 'test_provider',
- provider_id: 'TEST_' . $part->getId(),
- name: $part->getName() ?? 'Test Part',
- description: 'Test description',
- manufacturer: 'Test Mfg',
- mpn: 'MPN-' . $part->getId(),
- provider_url: 'https://example.com/' . $part->getId(),
- preview_image_url: null,
- ),
- sourceField: 'mpn',
- sourceKeyword: $part->getName() ?? 'test',
- localPart: null,
- ),
- ]
- );
- }
-
- $job->setSearchResults(new BulkSearchResponseDTO($partResults));
- $entityManager->persist($job);
- $entityManager->flush();
-
- return $job;
- }
-
- private function cleanupJob(object $entityManager, int $jobId): void
- {
- $entityManager->clear();
- $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($persistedJob) {
- $entityManager->remove($persistedJob);
- $entityManager->flush();
- }
- }
-
- public function testDeleteCompletedJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'completed');
- $jobId = $job->getId();
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/delete');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
-
- // Verify job was deleted
- $entityManager->clear();
- $this->assertNull($entityManager->find(BulkInfoProviderImportJob::class, $jobId));
- }
-
- public function testDeleteActiveJobFails(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'in_progress');
- $jobId = $job->getId();
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/delete');
- $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testDeleteNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/999999/delete');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- }
-
- public function testStopInProgressJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'in_progress');
- $jobId = $job->getId();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/stop');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
-
- // Verify job is stopped
- $entityManager->clear();
- $stoppedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- $this->assertTrue($stoppedJob->isStopped());
-
- $entityManager->remove($stoppedJob);
- $entityManager->flush();
- }
-
- public function testStopNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/stop');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- }
-
- public function testMarkPartCompletedAutoCompletesJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
- $partId = $parts[0]->getId();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-completed');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertEquals(1, $response['completed_count']);
- $this->assertTrue($response['job_completed']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testMarkPartSkippedWithReason(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
- $partId = $parts[0]->getId();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-skipped', [
- 'reason' => 'Not needed'
- ]);
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertEquals(1, $response['skipped_count']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testMarkPartPendingAfterCompleted(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
- $partId = $parts[0]->getId();
-
- // First mark as completed
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-completed');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
-
- // Then mark as pending again
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/mark-pending');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertEquals(0, $response['completed_count']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testMarkPartCompletedNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/mark-completed');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- }
-
- public function testQuickApplyWithValidJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
- $partId = $parts[0]->getId();
-
- // Quick apply will fail because test_provider doesn't exist, but it exercises the code path
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/quick-apply', [], [], [
- 'CONTENT_TYPE' => 'application/json',
- ], json_encode(['providerKey' => 'test_provider', 'providerId' => 'TEST_1']));
-
- // Will get 500 because test_provider doesn't exist, which exercises the catch block
- $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertFalse($response['success']);
- $this->assertStringContainsString('Quick apply failed', $response['error']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testQuickApplyFallsBackToTopResult(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
- $partId = $parts[0]->getId();
-
- // No providerKey/providerId in body - should fall back to top search result
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/quick-apply', [], [], [
- 'CONTENT_TYPE' => 'application/json',
- ], '{}');
-
- // Will get 500 because test_provider doesn't exist, but exercises the fallback code path
- $this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertStringContainsString('Quick apply failed', $response['error']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testQuickApplyEmptyResultsReturns400(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- // Create job with empty search results
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([
- new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [])
- ]));
- $entityManager->persist($job);
- $entityManager->flush();
-
- $jobId = $job->getId();
- $partId = $parts[0]->getId();
-
- // No provider specified and no search results - should return 400
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/' . $partId . '/quick-apply', [], [], [
- 'CONTENT_TYPE' => 'application/json',
- ], '{}');
- $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertStringContainsString('No search result available', $response['error']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testQuickApplyNonExistentPart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/999999/quick-apply');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testQuickApplyAllWithValidJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
-
- // Quick apply all - will fail for test_provider but exercises the code path
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/quick-apply-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- // Should have 1 failed (because test_provider doesn't exist)
- $this->assertEquals(1, $response['failed']);
- $this->assertNotEmpty($response['errors']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testQuickApplyAllWithNoSearchResults(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- // Create job with empty results
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([
- new BulkSearchPartResultsDTO(part: $parts[0], searchResults: [])
- ]));
- $entityManager->persist($job);
- $entityManager->flush();
-
- $jobId = $job->getId();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/quick-apply-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertEquals(0, $response['applied']);
- $this->assertEquals(1, $response['no_results']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testQuickApplyAllNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/quick-apply-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- }
-
- public function testQuickApplyAllSkipsCompletedParts(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
-
- // Mark the part as completed first
- $job->markPartAsCompleted($parts[0]->getId());
- $entityManager->flush();
-
- // Quick apply all should skip already-completed parts
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/quick-apply-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertEquals(0, $response['applied']);
- $this->assertEquals(0, $response['failed']);
- $this->assertEquals(0, $response['no_results']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testDeleteStoppedJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts, 'stopped');
- $jobId = $job->getId();
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/delete');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
-
- $entityManager->clear();
- $this->assertNull($entityManager->find(BulkInfoProviderImportJob::class, $jobId));
- }
-
- public function testManagePageSplitsActiveAndHistory(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- // Create one active and one completed job
- $activeJob = $this->createJobWithSearchResults($entityManager, $user, $parts, 'in_progress');
- $completedJob = $this->createJobWithSearchResults($entityManager, $user, $parts, 'completed');
-
- $client->request('GET', '/en/tools/bulk_info_provider_import/manage');
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
-
- $content = (string) $client->getResponse()->getContent();
- $this->assertStringContainsString('Active Jobs', $content);
- $this->assertStringContainsString('History', $content);
-
- $this->cleanupJob($entityManager, $activeJob->getId());
- $this->cleanupJob($entityManager, $completedJob->getId());
- }
-
- public function testManagePageCleansUpPendingJobsWithNoResults(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- // Create a pending job with no results (should be cleaned up)
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::PENDING);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
- $entityManager->persist($job);
- $entityManager->flush();
- $jobId = $job->getId();
-
- // Visit manage page - should trigger cleanup
- $client->request('GET', '/en/tools/bulk_info_provider_import/manage');
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
-
- // Verify the stale job was cleaned up
- $entityManager->clear();
- $this->assertNull($entityManager->find(BulkInfoProviderImportJob::class, $jobId));
- }
-
- public function testStep2RedirectsForNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/en/tools/bulk_info_provider_import/step2/999999');
-
- // Should redirect with error flash
- $this->assertResponseRedirects();
- }
-
- public function testStep2WithOtherUsersJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $otherUser = $entityManager->getRepository(User::class)->findOneBy(['name' => 'noread']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$otherUser || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $otherUser, $parts);
- $jobId = $job->getId();
-
- $client->request('GET', '/en/tools/bulk_info_provider_import/step2/' . $jobId);
-
- // Should redirect with access denied
- $this->assertResponseRedirects();
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testResearchPartNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/part/1/research');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- }
-
- public function testResearchPartNonExistentPart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/part/999999/research');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- $this->cleanupJob($entityManager, $jobId);
- }
-
- public function testResearchAllNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/research-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- }
-
- public function testResearchAllWithAllPartsCompleted(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $user = $entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']);
- $parts = $this->getTestParts($entityManager, [1]);
-
- if (!$user || empty($parts)) {
- $this->markTestSkipped('Required fixtures not found');
- }
-
- $job = $this->createJobWithSearchResults($entityManager, $user, $parts);
- $jobId = $job->getId();
-
- // Mark all parts as completed
- foreach ($parts as $part) {
- $job->markPartAsCompleted($part->getId());
- }
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $jobId . '/research-all');
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertEquals(0, $response['researched_count']);
-
- $this->cleanupJob($entityManager, $jobId);
- }
}
diff --git a/tests/Controller/KicadListEditorControllerTest.php b/tests/Controller/KicadListEditorControllerTest.php
deleted file mode 100644
index 0aa05aa1..00000000
--- a/tests/Controller/KicadListEditorControllerTest.php
+++ /dev/null
@@ -1,162 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Controller;
-
-use App\Entity\UserSystem\User;
-use App\Settings\MiscSettings\KiCadEDASettings;
-use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface;
-use PHPUnit\Framework\Attributes\Group;
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-
-#[Group('slow')]
-#[Group('DB')]
-final class KicadListEditorControllerTest extends WebTestCase
-{
- private string $footprintsPath;
- private string $symbolsPath;
- private string $customFootprintsPath;
- private string $customSymbolsPath;
- private string $originalFootprints;
- private string $originalSymbols;
- private string $originalCustomFootprints;
- private string $originalCustomSymbols;
- private bool $originalUseCustomList;
-
- protected function setUp(): void
- {
- parent::setUp();
-
- $projectDir = dirname(__DIR__, 2);
- $this->footprintsPath = $projectDir . '/public/kicad/footprints.txt';
- $this->symbolsPath = $projectDir . '/public/kicad/symbols.txt';
- $this->customFootprintsPath = $projectDir . '/public/kicad/footprints_custom.txt';
- $this->customSymbolsPath = $projectDir . '/public/kicad/symbols_custom.txt';
- $this->originalFootprints = (string) file_get_contents($this->footprintsPath);
- $this->originalSymbols = (string) file_get_contents($this->symbolsPath);
- $this->originalCustomFootprints = is_file($this->customFootprintsPath) ? (string) file_get_contents($this->customFootprintsPath) : '';
- $this->originalCustomSymbols = is_file($this->customSymbolsPath) ? (string) file_get_contents($this->customSymbolsPath) : '';
-
- static::bootKernel();
- /** @var SettingsManagerInterface $settingsManager */
- $settingsManager = static::getContainer()->get(SettingsManagerInterface::class);
- /** @var KiCadEDASettings $settings */
- $settings = $settingsManager->get(KiCadEDASettings::class);
- $this->originalUseCustomList = $settings->useCustomList;
- static::ensureKernelShutdown();
- }
-
- protected function tearDown(): void
- {
- file_put_contents($this->footprintsPath, $this->originalFootprints);
- file_put_contents($this->symbolsPath, $this->originalSymbols);
- file_put_contents($this->customFootprintsPath, $this->originalCustomFootprints);
- file_put_contents($this->customSymbolsPath, $this->originalCustomSymbols);
-
- static::bootKernel();
- /** @var SettingsManagerInterface $settingsManager */
- $settingsManager = static::getContainer()->get(SettingsManagerInterface::class);
- /** @var KiCadEDASettings $settings */
- $settings = $settingsManager->get(KiCadEDASettings::class);
- $settings->useCustomList = $this->originalUseCustomList;
- $settingsManager->save($settings);
- static::ensureKernelShutdown();
-
- parent::tearDown();
- }
-
- public function testEditorRequiresAuthentication(): void
- {
- $client = static::createClient();
- $client->request('GET', '/en/settings/misc/kicad-lists');
-
- $this->assertResponseStatusCodeSame(401);
- }
-
- public function testEditorAccessibleByAdmin(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/en/settings/misc/kicad-lists');
-
- $this->assertResponseIsSuccessful();
- $this->assertSelectorExists('form[name="kicad_list_editor"]');
- }
-
- public function testEditorShowsDefaultAndCustomFiles(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- file_put_contents($this->footprintsPath, "DefaultFootprint\n");
- file_put_contents($this->symbolsPath, "DefaultSymbol\n");
- file_put_contents($this->customFootprintsPath, "CustomFootprint\n");
- file_put_contents($this->customSymbolsPath, "CustomSymbol\n");
-
- $crawler = $client->request('GET', '/en/settings/misc/kicad-lists');
-
- $this->assertSame("CustomFootprint\n", $crawler->filter('#kicad_list_editor_customFootprints')->getNode(0)->nodeValue);
- $this->assertSame("CustomSymbol\n", $crawler->filter('#kicad_list_editor_customSymbols')->getNode(0)->nodeValue);
- $this->assertSame("DefaultFootprint\n", $crawler->filter('#kicad_list_editor_defaultFootprints')->getNode(0)->nodeValue);
- $this->assertSame("DefaultSymbol\n", $crawler->filter('#kicad_list_editor_defaultSymbols')->getNode(0)->nodeValue);
- }
-
- public function testEditorSavesCustomFilesAndSetting(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $crawler = $client->request('GET', '/en/settings/misc/kicad-lists');
- $form = $crawler->filter('form[name="kicad_list_editor"]')->form();
- $form['kicad_list_editor[customFootprints]'] = "Package_DIP:DIP-8_W7.62mm\n";
- $form['kicad_list_editor[customSymbols]'] = "Device:R\n";
- $form['kicad_list_editor[useCustomList]']->tick();
-
- $client->submit($form);
-
- $this->assertResponseRedirects('/en/settings/misc/kicad-lists');
- $this->assertSame("Package_DIP:DIP-8_W7.62mm\n", (string) file_get_contents($this->customFootprintsPath));
- $this->assertSame("Device:R\n", (string) file_get_contents($this->customSymbolsPath));
- $this->assertSame($this->originalFootprints, (string) file_get_contents($this->footprintsPath));
- $this->assertSame($this->originalSymbols, (string) file_get_contents($this->symbolsPath));
-
- /** @var SettingsManagerInterface $settingsManager */
- $settingsManager = $client->getContainer()->get(SettingsManagerInterface::class);
- /** @var KiCadEDASettings $settings */
- $settings = $settingsManager->reload(KiCadEDASettings::class);
- $this->assertTrue($settings->useCustomList);
- }
-
- private function loginAsUser($client, string $username): void
- {
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => $username]);
-
- if (!$user) {
- $this->markTestSkipped(sprintf('User "%s" not found in fixtures', $username));
- }
-
- $client->loginUser($user);
- }
-}
diff --git a/tests/Controller/SelectApiControllerTest.php b/tests/Controller/SelectApiControllerTest.php
deleted file mode 100644
index b07053b9..00000000
--- a/tests/Controller/SelectApiControllerTest.php
+++ /dev/null
@@ -1,152 +0,0 @@
-.
- */
-
-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();
- }
-}
diff --git a/tests/Controller/TypeaheadControllerTest.php b/tests/Controller/TypeaheadControllerTest.php
deleted file mode 100644
index ce2747fa..00000000
--- a/tests/Controller/TypeaheadControllerTest.php
+++ /dev/null
@@ -1,162 +0,0 @@
-.
- */
-
-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();
- }
-}
diff --git a/tests/Doctrine/Functions/AbstractDoctrineFunctionTestCase.php b/tests/Doctrine/Functions/AbstractDoctrineFunctionTestCase.php
deleted file mode 100644
index 7bc3d628..00000000
--- a/tests/Doctrine/Functions/AbstractDoctrineFunctionTestCase.php
+++ /dev/null
@@ -1,68 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Doctrine\Functions;
-
-use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\ORM\Query\AST\Node;
-use Doctrine\ORM\Query\SqlWalker;
-use PHPUnit\Framework\TestCase;
-
-abstract class AbstractDoctrineFunctionTestCase extends TestCase
-{
- protected function createSqlWalker(AbstractPlatform $platform, string $serverVersion = '11.0.0-MariaDB'): SqlWalker
- {
- $connection = $this->createMock(Connection::class);
- $connection->method('getDatabasePlatform')->willReturn($platform);
- $connection->method('getServerVersion')->willReturn($serverVersion);
-
- $sqlWalker = $this->getMockBuilder(SqlWalker::class)
- ->disableOriginalConstructor()
- ->onlyMethods(['getConnection'])
- ->getMock();
-
- $sqlWalker->method('getConnection')->willReturn($connection);
-
- return $sqlWalker;
- }
-
- protected function createNode(string $sql): Node
- {
- $node = $this->createMock(Node::class);
- $node->method('dispatch')->willReturn($sql);
-
- return $node;
- }
-
- protected function setObjectProperty(object $object, string $property, mixed $value): void
- {
- $reflection = new \ReflectionProperty($object, $property);
- $reflection->setValue($object, $value);
- }
-
- protected function setStaticProperty(string $class, string $property, mixed $value): void
- {
- $reflection = new \ReflectionProperty($class, $property);
- $reflection->setValue(null, $value);
- }
-}
diff --git a/tests/Doctrine/Functions/ArrayPositionTest.php b/tests/Doctrine/Functions/ArrayPositionTest.php
deleted file mode 100644
index 7fdff42d..00000000
--- a/tests/Doctrine/Functions/ArrayPositionTest.php
+++ /dev/null
@@ -1,42 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-
-namespace App\Tests\Doctrine\Functions;
-
-use App\Doctrine\Functions\ArrayPosition;
-use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
-
-final class ArrayPositionTest extends AbstractDoctrineFunctionTestCase
-{
- public function testArrayPositionBuildsSql(): void
- {
- $function = new ArrayPosition('ARRAY_POSITION');
- $this->setObjectProperty($function, 'array', $this->createNode(':ids'));
- $this->setObjectProperty($function, 'field', $this->createNode('p.id'));
-
- $sql = $function->getSql($this->createSqlWalker(new PostgreSQLPlatform()));
-
- $this->assertSame('ARRAY_POSITION(:ids, p.id)', $sql);
- }
-}
-
diff --git a/tests/Doctrine/Functions/Field2Test.php b/tests/Doctrine/Functions/Field2Test.php
deleted file mode 100644
index d25e511f..00000000
--- a/tests/Doctrine/Functions/Field2Test.php
+++ /dev/null
@@ -1,45 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Doctrine\Functions;
-
-use App\Doctrine\Functions\Field2;
-use Doctrine\DBAL\Platforms\MySQLPlatform;
-
-final class Field2Test extends AbstractDoctrineFunctionTestCase
-{
- public function testField2BuildsSql(): void
- {
- $function = new Field2('FIELD2');
- $this->setObjectProperty($function, 'field', $this->createNode('p.id'));
- $this->setObjectProperty($function, 'values', [
- $this->createNode('1'),
- $this->createNode('2'),
- $this->createNode('3'),
- ]);
-
- $sql = $function->getSql($this->createSqlWalker(new MySQLPlatform()));
-
- $this->assertSame('FIELD2(p.id, 1, 2, 3)', $sql);
- }
-}
-
diff --git a/tests/Doctrine/Functions/ILikeTest.php b/tests/Doctrine/Functions/ILikeTest.php
deleted file mode 100644
index 4541e9c9..00000000
--- a/tests/Doctrine/Functions/ILikeTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Doctrine\Functions;
-
-use App\Doctrine\Functions\ILike;
-use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySQLPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
-use Doctrine\DBAL\Platforms\SQLitePlatform;
-use Doctrine\DBAL\Platforms\SQLServerPlatform;
-use PHPUnit\Framework\Attributes\DataProvider;
-
-final class ILikeTest extends AbstractDoctrineFunctionTestCase
-{
- public static function iLikePlatformProvider(): \Generator
- {
- yield 'mysql' => [new MySQLPlatform(), '(part_name LIKE :pattern)'];
- yield 'postgres' => [new PostgreSQLPlatform(), '(part_name ILIKE :pattern)'];
- yield 'sqlite' => [new SQLitePlatform(), "(part_name LIKE :pattern ESCAPE '\\')"];
- }
-
- #[DataProvider('iLikePlatformProvider')]
- public function testILikeUsesExpectedOperator(AbstractPlatform $platform, string $expectedSql): void
- {
- $function = new ILike('ILIKE');
- $function->value = $this->createNode('part_name');
- $function->expr = $this->createNode(':pattern');
-
- $sql = $function->getSql($this->createSqlWalker($platform));
-
- $this->assertSame($expectedSql, $sql);
- }
-
- public function testILikeThrowsOnUnsupportedPlatform(): void
- {
- $function = new ILike('ILIKE');
- $function->value = $this->createNode('part_name');
- $function->expr = $this->createNode(':pattern');
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('does not support case insensitive like expressions');
-
- $function->getSql($this->createSqlWalker(new SQLServerPlatform()));
- }
-}
-
diff --git a/tests/Doctrine/Functions/NatsortTest.php b/tests/Doctrine/Functions/NatsortTest.php
deleted file mode 100644
index fd10199f..00000000
--- a/tests/Doctrine/Functions/NatsortTest.php
+++ /dev/null
@@ -1,95 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Doctrine\Functions;
-
-use App\Doctrine\Functions\Natsort;
-use Doctrine\DBAL\Platforms\MariaDBPlatform;
-use Doctrine\DBAL\Platforms\MySQLPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
-use Doctrine\DBAL\Platforms\SQLitePlatform;
-
-final class NatsortTest extends AbstractDoctrineFunctionTestCase
-{
- protected function setUp(): void
- {
- parent::setUp();
-
- Natsort::allowSlowNaturalSort(false);
- $this->setStaticProperty(Natsort::class, 'supportsNaturalSort', null);
- }
-
- public function testNatsortUsesPostgresCollation(): void
- {
- $function = new Natsort('NATSORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new PostgreSQLPlatform()));
-
- $this->assertSame('part_name COLLATE numeric', $sql);
- }
-
- public function testNatsortUsesMariaDbNativeFunctionOnSupportedVersion(): void
- {
- $function = new Natsort('NATSORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new MariaDBPlatform(), '10.11.2-MariaDB'));
-
- $this->assertSame('NATURAL_SORT_KEY(part_name)', $sql);
- }
-
- public function testNatsortFallsBackWithoutSlowSort(): void
- {
- $function = new Natsort('NATSORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new MariaDBPlatform(), '10.6.10-MariaDB'));
-
- $this->assertSame('part_name', $sql);
- }
-
- public function testNatsortUsesSlowSortFunctionOnMySqlWhenEnabled(): void
- {
- Natsort::allowSlowNaturalSort();
-
- $function = new Natsort('NATSORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new MySQLPlatform()));
-
- $this->assertSame('NatSortKey(part_name, 0)', $sql);
- }
-
- public function testNatsortUsesSlowSortCollationOnSqliteWhenEnabled(): void
- {
- Natsort::allowSlowNaturalSort();
-
- $function = new Natsort('NATSORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new SQLitePlatform()));
-
- $this->assertSame('part_name COLLATE NATURAL_CMP', $sql);
- }
-}
-
diff --git a/tests/Doctrine/Functions/RegexpTest.php b/tests/Doctrine/Functions/RegexpTest.php
deleted file mode 100644
index d1866210..00000000
--- a/tests/Doctrine/Functions/RegexpTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Doctrine\Functions;
-
-use App\Doctrine\Functions\Regexp;
-use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySQLPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
-use Doctrine\DBAL\Platforms\SQLitePlatform;
-use Doctrine\DBAL\Platforms\SQLServerPlatform;
-use PHPUnit\Framework\Attributes\DataProvider;
-
-final class RegexpTest extends AbstractDoctrineFunctionTestCase
-{
- public static function regexpPlatformProvider(): \Generator
- {
- yield 'mysql' => [new MySQLPlatform(), '(part_name REGEXP :regex)'];
- yield 'sqlite' => [new SQLitePlatform(), '(part_name REGEXP :regex)'];
- yield 'postgres' => [new PostgreSQLPlatform(), '(part_name ~* :regex)'];
- }
-
- #[DataProvider('regexpPlatformProvider')]
- public function testRegexpUsesExpectedOperator(AbstractPlatform $platform, string $expectedSql): void
- {
- $function = new Regexp('REGEXP');
- $this->setObjectProperty($function, 'value', $this->createNode('part_name'));
- $this->setObjectProperty($function, 'regexp', $this->createNode(':regex'));
-
- $sql = $function->getSql($this->createSqlWalker($platform));
-
- $this->assertSame($expectedSql, $sql);
- }
-
- public function testRegexpThrowsOnUnsupportedPlatform(): void
- {
- $function = new Regexp('REGEXP');
- $this->setObjectProperty($function, 'value', $this->createNode('part_name'));
- $this->setObjectProperty($function, 'regexp', $this->createNode(':regex'));
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('does not support regular expressions');
-
- $function->getSql($this->createSqlWalker(new SQLServerPlatform()));
- }
-}
-
diff --git a/tests/Doctrine/Functions/SiValueSortTest.php b/tests/Doctrine/Functions/SiValueSortTest.php
deleted file mode 100644
index dbdd9d28..00000000
--- a/tests/Doctrine/Functions/SiValueSortTest.php
+++ /dev/null
@@ -1,193 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Doctrine\Functions;
-
-use App\Doctrine\Functions\SiValueSort;
-use Doctrine\DBAL\Platforms\MySQLPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
-use Doctrine\DBAL\Platforms\SQLitePlatform;
-
-final class SiValueSortTest extends AbstractDoctrineFunctionTestCase
-{
- public function testPostgreSQLGeneratesCaseExpression(): void
- {
- $function = new SiValueSort('SI_VALUE_SORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new PostgreSQLPlatform()));
-
- $this->assertStringContainsString('CASE', $sql);
- $this->assertStringContainsString("REPLACE(part_name, ',', '.')", $sql);
- $this->assertStringContainsString('1e-12', $sql);
- $this->assertStringContainsString('1e-9', $sql);
- $this->assertStringContainsString('1e-6', $sql);
- $this->assertStringContainsString('1e-3', $sql);
- $this->assertStringContainsString('1e3', $sql);
- $this->assertStringContainsString('1e6', $sql);
- $this->assertStringContainsString('1e9', $sql);
- $this->assertStringContainsString('1e12', $sql);
- }
-
- public function testMySQLGeneratesCaseExpression(): void
- {
- $function = new SiValueSort('SI_VALUE_SORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new MySQLPlatform()));
-
- $this->assertStringContainsString('CASE', $sql);
- $this->assertStringContainsString("REPLACE(part_name, ',', '.')", $sql);
- $this->assertStringContainsString('1e-12', $sql);
- $this->assertStringContainsString('1e6', $sql);
- }
-
- public function testSQLiteUsesSiValueFunction(): void
- {
- $function = new SiValueSort('SI_VALUE_SORT');
- $this->setObjectProperty($function, 'field', $this->createNode('part_name'));
-
- $sql = $function->getSql($this->createSqlWalker(new SQLitePlatform()));
-
- $this->assertSame('SI_VALUE(part_name)', $sql);
- }
-
- /**
- * @dataProvider sqliteSiValueProvider
- */
- public function testSqliteSiValue(?string $input, ?float $expected): void
- {
- $result = SiValueSort::sqliteSiValue($input);
-
- if ($expected === null) {
- $this->assertNull($result);
- } else {
- $this->assertEqualsWithDelta($expected, $result, $expected * 1e-9);
- }
- }
-
- /**
- * @return iterable
- */
- public static function sqliteSiValueProvider(): iterable
- {
- // Basic SI prefix values
- yield 'pico' => ['10pF', 10e-12];
- yield 'nano' => ['100nF', 100e-9];
- yield 'micro_u' => ['1uF', 1e-6];
- yield 'micro_µ' => ['1µF', 1e-6];
- yield 'milli' => ['4.7mH', 4.7e-3];
- yield 'kilo_lower' => ['4.7k', 4.7e3];
- yield 'kilo_upper' => ['4.7K', 4.7e3];
- yield 'mega' => ['1M', 1e6];
- yield 'giga' => ['2.2G', 2.2e9];
- yield 'tera' => ['1T', 1e12];
-
- // No prefix (plain number)
- yield 'plain_integer' => ['100', 100.0];
- yield 'plain_decimal' => ['4.7', 4.7];
-
- // Decimal values with prefix (dot separator)
- yield 'decimal_nano' => ['4.7nF', 4.7e-9];
- yield 'decimal_micro' => ['0.1uF', 0.1e-6];
- yield 'decimal_kilo' => ['2.2k', 2.2e3];
-
- // Comma decimal separator (European locale)
- yield 'comma_kilo' => ['4,7k', 4.7e3];
- yield 'comma_micro' => ['2,2uF', 2.2e-6];
- yield 'comma_kilo_space' => ['1,2 kΩ', 1.2e3];
-
- // Number NOT at the start — should return NULL
- yield 'prefixed_name' => ['CAP-100nF', null];
- yield 'name_with_number' => ['R 4.7k 1%', null];
- yield 'crystal' => ['Crystal 20MHz', null];
-
- // Number at start with trailing text
- yield 'number_with_suffix' => ['10nF 25V', 10e-9];
-
- // Space between number and prefix
- yield 'space_before_prefix' => ['100 nF', 100e-9];
-
- // Leading whitespace before number
- yield 'leading_whitespace' => [' 10uF', 10e-6];
-
- // No number at all
- yield 'no_number' => ['Connector', null];
- yield 'text_only' => ['LED red', null];
-
- // Null input
- yield 'null' => [null, null];
-
- // Empty string
- yield 'empty' => ['', null];
- }
-
- /**
- * Test that the sort order is correct by comparing sqliteSiValue results.
- */
- public function testSortOrder(): void
- {
- $parts = ['1uF', '100nF', '10pF', '10uF', '0.1mF', '1F', '10kF', '1MF'];
- $expected = ['10pF', '100nF', '1uF', '10uF', '0.1mF', '1F', '10kF', '1MF'];
-
- // Sort using sqliteSiValue
- usort($parts, static function (string $a, string $b): int {
- $va = SiValueSort::sqliteSiValue($a);
- $vb = SiValueSort::sqliteSiValue($b);
- return $va <=> $vb;
- });
-
- $this->assertSame($expected, $parts);
- }
-
- /**
- * Test that NULL values sort last (after all numeric values).
- */
- public function testNullSortsLast(): void
- {
- $parts = ['Connector', '100nF', 'LED red', '10pF'];
-
- usort($parts, static function (string $a, string $b): int {
- $va = SiValueSort::sqliteSiValue($a);
- $vb = SiValueSort::sqliteSiValue($b);
-
- // NULL sorts last
- if ($va === null && $vb === null) {
- return 0;
- }
- if ($va === null) {
- return 1;
- }
- if ($vb === null) {
- return -1;
- }
-
- return $va <=> $vb;
- });
-
- $this->assertSame('10pF', $parts[0]);
- $this->assertSame('100nF', $parts[1]);
- // Last two should be the non-numeric names
- $this->assertContains('Connector', array_slice($parts, 2));
- $this->assertContains('LED red', array_slice($parts, 2));
- }
-}
diff --git a/tests/EventSubscriber/MaintenanceModeSubscriberTest.php b/tests/EventSubscriber/MaintenanceModeSubscriberTest.php
deleted file mode 100644
index 0d975ee0..00000000
--- a/tests/EventSubscriber/MaintenanceModeSubscriberTest.php
+++ /dev/null
@@ -1,103 +0,0 @@
-.
- */
-
-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');
- }
-}
diff --git a/tests/EventSubscriber/RedirectToHttpsSubscriberTest.php b/tests/EventSubscriber/RedirectToHttpsSubscriberTest.php
deleted file mode 100644
index ec782b66..00000000
--- a/tests/EventSubscriber/RedirectToHttpsSubscriberTest.php
+++ /dev/null
@@ -1,101 +0,0 @@
-.
- */
-
-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);
- }
-}
diff --git a/tests/Services/AI/AIPlatformRegistryTest.php b/tests/Services/AI/AIPlatformRegistryTest.php
deleted file mode 100644
index 1577f9b5..00000000
--- a/tests/Services/AI/AIPlatformRegistryTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
-.
- */
-
-/**
- * Tests for App\Services\AI\AIPlatformRegistry
- */
-declare(strict_types=1);
-
-namespace App\Tests\Services\AI;
-
-use App\Services\AI\AIPlatformRegistry;
-use App\Services\AI\AIPlatforms;
-use App\Services\AI\AIPlatformSettingsInterface;
-use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface;
-use PHPUnit\Framework\TestCase;
-use Symfony\AI\Platform\PlatformInterface;
-
-class AIPlatformRegistryTest extends TestCase
-{
- public function testRegistersEnabledPlatformsAndReturnsPlatform(): void
- {
- // Create a platform mock and expose it under the service tag name (openrouter)
- $platformMock = $this->createMock(PlatformInterface::class);
-
- // Settings for OpenRouter -> enabled
- $openRouterSettings = $this->createMock(AIPlatformSettingsInterface::class);
- $openRouterSettings->method('isAIPlatformEnabled')->willReturn(true);
-
- // Settings for LMStudio -> disabled
- $lmSettings = $this->createMock(AIPlatformSettingsInterface::class);
- $lmSettings->method('isAIPlatformEnabled')->willReturn(false);
-
- // Settings manager should return the corresponding settings object depending on the requested class name
- $settingsManager = $this->createMock(SettingsManagerInterface::class);
- $settingsManager->method('get')->willReturnMap([
- [AIPlatforms::OPENROUTER->toSettingsClass(), $openRouterSettings],
- [AIPlatforms::LMSTUDIO->toSettingsClass(), $lmSettings],
- ]);
-
- $platforms = new \ArrayIterator([
- AIPlatforms::OPENROUTER->toServiceTagName() => $platformMock,
- ]);
-
- $registry = new AIPlatformRegistry($settingsManager, $platforms);
-
- // OPENROUTER should be enabled and retrievable
- $this->assertTrue($registry->isEnabled(AIPlatforms::OPENROUTER));
- $this->assertSame($platformMock, $registry->getPlatform(AIPlatforms::OPENROUTER));
-
- // LMSTUDIO is either not registered or disabled -> should not be enabled
- $this->assertFalse($registry->isEnabled(AIPlatforms::LMSTUDIO));
- $this->expectException(\InvalidArgumentException::class);
- $registry->getPlatform(AIPlatforms::LMSTUDIO);
- }
-
- public function testGetEnabledPlatformsReturnsIndexedArray(): void
- {
- $platformMock = $this->createMock(PlatformInterface::class);
-
- $openRouterSettings = $this->createMock(AIPlatformSettingsInterface::class);
- $openRouterSettings->method('isAIPlatformEnabled')->willReturn(true);
-
- $settingsManager = $this->createMock(SettingsManagerInterface::class);
- $settingsManager->method('get')->willReturnMap([
- [AIPlatforms::OPENROUTER->toSettingsClass(), $openRouterSettings],
- [AIPlatforms::LMSTUDIO->toSettingsClass(), $this->createMock(AIPlatformSettingsInterface::class)],
- ]);
-
- $platforms = new \ArrayIterator([
- AIPlatforms::OPENROUTER->toServiceTagName() => $platformMock,
- // lmstudio not registered
- ]);
-
- $registry = new AIPlatformRegistry($settingsManager, $platforms);
-
- $enabled = $registry->getEnabledPlatforms();
-
- $this->assertArrayHasKey(AIPlatforms::OPENROUTER->value, $enabled);
- $this->assertSame($platformMock, $enabled[AIPlatforms::OPENROUTER->value]);
- }
-}
-
diff --git a/tests/Services/Cache/ElementCacheTagGeneratorTest.php b/tests/Services/Cache/ElementCacheTagGeneratorTest.php
deleted file mode 100644
index f747441f..00000000
--- a/tests/Services/Cache/ElementCacheTagGeneratorTest.php
+++ /dev/null
@@ -1,67 +0,0 @@
-.
- */
-
-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');
- }
-}
diff --git a/tests/Services/Cache/UserCacheKeyGeneratorTest.php b/tests/Services/Cache/UserCacheKeyGeneratorTest.php
deleted file mode 100644
index 23583db4..00000000
--- a/tests/Services/Cache/UserCacheKeyGeneratorTest.php
+++ /dev/null
@@ -1,110 +0,0 @@
-.
- */
-
-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);
- }
-}
diff --git a/tests/Services/EntityURLGeneratorTest.php b/tests/Services/EntityURLGeneratorTest.php
deleted file mode 100644
index f21511e0..00000000
--- a/tests/Services/EntityURLGeneratorTest.php
+++ /dev/null
@@ -1,113 +0,0 @@
-.
- */
-
-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);
- }
-}
diff --git a/tests/Services/Formatters/MarkdownParserTest.php b/tests/Services/Formatters/MarkdownParserTest.php
deleted file mode 100644
index 0b27972f..00000000
--- a/tests/Services/Formatters/MarkdownParserTest.php
+++ /dev/null
@@ -1,86 +0,0 @@
-.
- */
-
-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('');
- // The raw < should not appear unescaped inside the attribute
- $this->assertStringNotContainsString('