Skip to content

Commit e539022

Browse files
committed
test: added more tests to avoid missing code coverage
1 parent 4d607a5 commit e539022

36 files changed

Lines changed: 1895 additions & 247 deletions

phpmyfaq/src/phpMyFAQ/Auth/OAuth2/AuthorizationServer.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ final class AuthorizationServer
4242
{
4343
/** @var callable(Request): array{body: array<string, mixed>, status: int, headers?: array<string, string>}|null */
4444
private $tokenIssuer = null;
45+
/** @var callable(Request, string, bool): array{body: mixed, status: int, headers?: array<string, string>}|null */
46+
private $authorizationCompleter = null;
4547

4648
public function __construct(
4749
private readonly Configuration $configuration,
@@ -58,6 +60,16 @@ public function setTokenIssuer(callable $issuer): void
5860
$this->tokenIssuer = $issuer;
5961
}
6062

63+
/**
64+
* Allows integration code to provide an authorization completer implementation.
65+
*
66+
* @param callable(Request, string, bool): array{body: mixed, status: int, headers?: array<string, string>} $completer
67+
*/
68+
public function setAuthorizationCompleter(callable $completer): void
69+
{
70+
$this->authorizationCompleter = $completer;
71+
}
72+
6173
/**
6274
* Issues an OAuth2 access token response payload.
6375
*
@@ -160,6 +172,10 @@ public function isEnabled(): bool
160172

161173
public function completeAuthorization(Request $request, string $userId, bool $approved): array
162174
{
175+
if (is_callable($this->authorizationCompleter)) {
176+
return ($this->authorizationCompleter)($request, $userId, $approved);
177+
}
178+
163179
try {
164180
$psrRequest = $this->toPsr7Request($request);
165181
$server = $this->buildLeagueAuthorizationServer();

phpmyfaq/src/phpMyFAQ/Controller/Api/CategoryController.php

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,36 @@
3535

3636
final class CategoryController extends AbstractApiController
3737
{
38+
/** @var null|callable */
39+
private $categoryFactory = null;
40+
41+
/** @var null|callable */
42+
private $categoryPermissionFactory = null;
43+
44+
/** @var null|callable */
45+
private $orderFactory = null;
46+
3847
public function __construct(
3948
private readonly Language $language,
4049
) {
4150
parent::__construct();
4251
}
4352

53+
public function setCategoryFactory(callable $categoryFactory): void
54+
{
55+
$this->categoryFactory = $categoryFactory;
56+
}
57+
58+
public function setCategoryPermissionFactory(callable $categoryPermissionFactory): void
59+
{
60+
$this->categoryPermissionFactory = $categoryPermissionFactory;
61+
}
62+
63+
public function setOrderFactory(callable $orderFactory): void
64+
{
65+
$this->orderFactory = $orderFactory;
66+
}
67+
4468
/**
4569
* @throws \Exception
4670
*/
@@ -139,7 +163,7 @@ public function list(?Request $request = null): JsonResponse
139163

140164
[$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser);
141165

142-
$category = new Category($this->configuration, $currentGroups, withPermission: true);
166+
$category = $this->createCategory($currentGroups);
143167
$category->setUser($currentUser);
144168
$category->setGroups($currentGroups);
145169
$category->setLanguage($currentLanguage);
@@ -265,12 +289,12 @@ public function create(Request $request): JsonResponse
265289

266290
$currentLanguage = $this->configuration->getLanguage()->getLanguage();
267291

268-
$category = new Category($this->configuration, $currentGroups, withPermission: true);
292+
$category = $this->createCategory($currentGroups);
269293
$category->setUser($currentUser);
270294
$category->setGroups($currentGroups);
271295
$category->setLanguage($currentLanguage);
272296

273-
$categoryPermission = new CategoryPermission($this->configuration);
297+
$categoryPermission = $this->createCategoryPermission();
274298

275299
$languageCode = Filter::filterVar($data->language, FILTER_SANITIZE_SPECIAL_CHARS);
276300
$parentId = Filter::filterVar($data->{'parent-id'}, FILTER_VALIDATE_INT);
@@ -320,7 +344,7 @@ public function create(Request $request): JsonResponse
320344
$categoryId = $category->create($categoryEntity);
321345

322346
// Category Order entry
323-
$categoryOrder = new Order($this->configuration);
347+
$categoryOrder = $this->createOrder();
324348
$categoryOrder->add($categoryId, $parentId);
325349

326350
if ($categoryId) {
@@ -339,4 +363,31 @@ public function create(Request $request): JsonResponse
339363
];
340364
return $this->json($result, Response::HTTP_BAD_REQUEST);
341365
}
366+
367+
private function createCategory(array $currentGroups): Category
368+
{
369+
if (is_callable($this->categoryFactory)) {
370+
return ($this->categoryFactory)($currentGroups);
371+
}
372+
373+
return new Category($this->configuration, $currentGroups, withPermission: true);
374+
}
375+
376+
private function createCategoryPermission(): CategoryPermission
377+
{
378+
if (is_callable($this->categoryPermissionFactory)) {
379+
return ($this->categoryPermissionFactory)();
380+
}
381+
382+
return new CategoryPermission($this->configuration);
383+
}
384+
385+
private function createOrder(): Order
386+
{
387+
if (is_callable($this->orderFactory)) {
388+
return ($this->orderFactory)();
389+
}
390+
391+
return new Order($this->configuration);
392+
}
342393
}

phpmyfaq/src/phpMyFAQ/Controller/Api/PdfController.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535

3636
final class PdfController extends AbstractController
3737
{
38+
/** @var null|callable(\phpMyFAQ\Configuration): Faq */
39+
private $faqFactory = null;
40+
/** @var null|callable(\phpMyFAQ\Configuration): Services */
41+
private $servicesFactory = null;
42+
3843
public function __construct()
3944
{
4045
parent::__construct();
@@ -44,6 +49,16 @@ public function __construct()
4449
}
4550
}
4651

52+
public function setFaqFactory(callable $faqFactory): void
53+
{
54+
$this->faqFactory = $faqFactory;
55+
}
56+
57+
public function setServicesFactory(callable $servicesFactory): void
58+
{
59+
$this->servicesFactory = $servicesFactory;
60+
}
61+
4762
/**
4863
* @throws Exception
4964
*/
@@ -88,7 +103,9 @@ public function getById(Request $request): JsonResponse
88103
{
89104
[$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser);
90105

91-
$faq = new Faq($this->configuration);
106+
$faq = is_callable($this->faqFactory)
107+
? ($this->faqFactory)($this->configuration)
108+
: new Faq($this->configuration);
92109
$faq->setUser($currentUser);
93110
$faq->setGroups($currentGroups);
94111

@@ -103,7 +120,9 @@ public function getById(Request $request): JsonResponse
103120
return $this->json($result, Response::HTTP_NOT_FOUND);
104121
}
105122

106-
$services = new Services($this->configuration);
123+
$services = is_callable($this->servicesFactory)
124+
? ($this->servicesFactory)($this->configuration)
125+
: new Services($this->configuration);
107126
$services->setFaqId($faqId);
108127
$services->setLanguage($this->configuration->getLanguage()->getLanguage());
109128
$services->setCategoryId($categoryId);

phpmyfaq/src/phpMyFAQ/Controller/Api/RegistrationController.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434

3535
final class RegistrationController extends AbstractController
3636
{
37+
/** @var null|callable(Configuration): RegistrationHelper */
38+
private $registrationHelperFactory = null;
39+
3740
public function __construct()
3841
{
3942
parent::__construct();
@@ -43,6 +46,11 @@ public function __construct()
4346
}
4447
}
4548

49+
public function setRegistrationHelperFactory(callable $registrationHelperFactory): void
50+
{
51+
$this->registrationHelperFactory = $registrationHelperFactory;
52+
}
53+
4654
/**
4755
* @throws TransportExceptionInterface
4856
* @throws Exception
@@ -108,7 +116,9 @@ public function create(Request $request): JsonResponse
108116
{
109117
$this->hasValidToken();
110118

111-
$registrationHelper = new RegistrationHelper($this->configuration);
119+
$registrationHelper = is_callable($this->registrationHelperFactory)
120+
? ($this->registrationHelperFactory)($this->configuration)
121+
: new RegistrationHelper($this->configuration);
112122

113123
$data = json_decode(json: $request->getContent(), associative: false, depth: 512, flags: JSON_THROW_ON_ERROR);
114124

phpmyfaq/src/phpMyFAQ/EventListener/LanguageListener.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,10 @@ private function initializeTranslation(string $currentLanguage): void
9191
{
9292
Strings::init($currentLanguage);
9393

94-
try {
95-
Translation::create()
96-
->setTranslationsDir(PMF_TRANSLATION_DIR)
97-
->setDefaultLanguage(defaultLanguage: 'en')
98-
->setCurrentLanguage($currentLanguage)
99-
->setMultiByteLanguage();
100-
} catch (Exception $exception) {
101-
throw $exception;
102-
}
94+
Translation::create()
95+
->setTranslationsDir(PMF_TRANSLATION_DIR)
96+
->setDefaultLanguage(defaultLanguage: 'en')
97+
->setCurrentLanguage($currentLanguage)
98+
->setMultiByteLanguage();
10399
}
104100
}

phpmyfaq/translations/language_de.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,7 @@
784784
$PMF_LANG['msgAllCatArticles'] = "FAQs in dieser Kategorie";
785785
$PMF_LANG['msgTagSearch'] = "FAQs mit gleichen Tags";
786786
$PMF_LANG['ad_pmf_info'] = "phpMyFAQ Information";
787+
$PMF_LANG['ad_pmf_news'] = 'phpMyFAQ Nachrichten';
787788
$PMF_LANG['msgOnlineVersionCheck'] = "Online Versionsüberprüfung";
788789
$PMF_LANG['ad_system_info'] = "System Information";
789790

@@ -1664,6 +1665,7 @@
16641665
$PMF_LANG['msgPushNewQuestion'] = 'Neue offene Frage eingereicht';
16651666
$PMF_LANG['msgPushNotificationsDescription'] = 'Erhalten Sie Browser-Benachrichtigungen, wenn neue FAQs veröffentlicht oder Fragen eingereicht werden.';
16661667
$PMF_LANG['pushControlCenter'] = 'Push';
1668+
$PMF_LANG['storageControlCenter'] = 'Speicher-Backend';
16671669
$PMF_LANG['msgGenerateVapidKeys'] = 'VAPID-Schlüssel generieren';
16681670
$PMF_LANG['msgVapidKeysGenerated'] = 'VAPID-Schlüssel wurden erfolgreich generiert.';
16691671
$PMF_LANG['msgVapidKeysError'] = 'VAPID-Schlüssel konnten nicht generiert werden.';
@@ -1681,4 +1683,15 @@
16811683
$LANG_CONF['mail.mailgunRegion'] = ['input', 'Mailgun Region', 'z.B. us, eu'];
16821684
$LANG_CONF['mail.useQueue'] = ['checkbox', 'Hintergrund-Worker-Warteschlange für die E-Mail-Zustellung verwenden'];
16831685

1686+
$LANG_CONF['storage.testRedisConnection'] = ['button', 'Teste die Verbindung zu Redis'];
1687+
$PMF_LANG['storage.testRedisConnection'] = 'Teste die Verbindung zu Redis';
1688+
1689+
$PMF_LANG['msgAdminHeaderLdap'] = 'LDAP Konfiguration';
1690+
$PMF_LANG['msgAdminLdapHealthCheck'] = 'LDAP Health Check';
1691+
$PMF_LANG['msgAdminLdapServers'] = 'LDAP Server';
1692+
$PMF_LANG['msgAdminLdapMapping'] = 'LDAP Attribute Mapping';
1693+
$PMF_LANG['msgAdminLdapOptions'] = 'LDAP Optionen';
1694+
$PMF_LANG['msgAdminLdapGroupSettings'] = 'LDAP Gruppeneinstellungen';
1695+
$PMF_LANG['msgAdminLdapSettings'] = 'Allgemeine LDAP-Einstellungen';
1696+
16841697
return $PMF_LANG;

phpmyfaq/translations/language_en.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1706,7 +1706,6 @@
17061706
$LANG_CONF['storage.testRedisConnection'] = ['button', 'Test Redis connection'];
17071707
$PMF_LANG['storage.testRedisConnection'] = 'Test Redis connection';
17081708

1709-
// added v4.1.0 - 2026-03-05 by Thorsten
17101709
$PMF_LANG['msgAdminHeaderLdap'] = 'LDAP Configuration';
17111710
$PMF_LANG['msgAdminLdapHealthCheck'] = 'LDAP Health Check';
17121711
$PMF_LANG['msgAdminLdapServers'] = 'LDAP Servers';

tests/phpMyFAQ/Controller/Api/AttachmentControllerTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
namespace phpMyFAQ\Controller\Api;
44

5+
use phpMyFAQ\Attachment\AttachmentFactory;
56
use phpMyFAQ\Configuration;
67
use phpMyFAQ\Database\Sqlite3;
8+
use phpMyFAQ\Language;
79
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
10+
use PHPUnit\Framework\Attributes\CoversClass;
11+
use PHPUnit\Framework\Attributes\UsesClass;
12+
use PHPUnit\Framework\Attributes\UsesNamespace;
813
use PHPUnit\Framework\TestCase;
914
use Symfony\Component\HttpFoundation\JsonResponse;
1015
use Symfony\Component\HttpFoundation\Request;
@@ -26,6 +31,10 @@ public function isApiEnabled(): bool
2631
}
2732

2833
#[AllowMockObjectsWithoutExpectations]
34+
#[CoversClass(AttachmentController::class)]
35+
#[UsesNamespace('phpMyFAQ')]
36+
#[UsesClass(AbstractApiController::class)]
37+
#[UsesClass(PaginatedResponseOptions::class)]
2938
class AttachmentControllerTest extends TestCase
3039
{
3140
private ?Sqlite3 $dbHandle = null;
@@ -41,6 +50,40 @@ protected function setUp(): void
4150
$this->dbHandle = new Sqlite3();
4251
$this->dbHandle->connect(PMF_TEST_DIR . '/test.db', '', '');
4352
new Configuration($this->dbHandle);
53+
54+
Language::$language = 'en';
55+
$this->setAttachmentFactoryStorageType(0);
56+
}
57+
58+
protected function tearDown(): void
59+
{
60+
$this->setAttachmentFactoryStorageType(0);
61+
62+
parent::tearDown();
63+
}
64+
65+
private function setAttachmentFactoryStorageType(?int $storageType): void
66+
{
67+
$reflection = new \ReflectionClass(AttachmentFactory::class);
68+
$property = $reflection->getProperty('storageType');
69+
$property->setValue(null, $storageType);
70+
}
71+
72+
private function insertAttachmentFixture(int $id, int $recordId): void
73+
{
74+
$query = sprintf(
75+
"INSERT INTO faqattachment (id, record_id, record_lang, real_hash, virtual_hash, password_hash, filename, filesize, encrypted, mime_type) VALUES (%d, %d, 'en', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', NULL, 'fixture-%d.txt', 123, 0, 'text/plain')",
76+
$id,
77+
$recordId,
78+
$id,
79+
);
80+
81+
$this->dbHandle?->query($query);
82+
}
83+
84+
private function deleteAttachmentFixture(int $id): void
85+
{
86+
$this->dbHandle?->query(sprintf('DELETE FROM faqattachment WHERE id = %d', $id));
4487
}
4588

4689
public function testConstructorWithApiEnabled(): void
@@ -255,4 +298,25 @@ public function testListResponseStructure(): void
255298
}
256299
}
257300
}
301+
302+
public function testListReturnsInternalServerErrorWhenAttachmentFactoryThrows(): void
303+
{
304+
$fixtureId = 990001;
305+
$this->insertAttachmentFixture($fixtureId, 1);
306+
$this->setAttachmentFactoryStorageType(999);
307+
308+
$request = new Request();
309+
$request->attributes->set('faqId', '1');
310+
311+
$controller = new AttachmentController();
312+
$response = $controller->list($request);
313+
$payload = json_decode($response->getContent(), true);
314+
315+
$this->deleteAttachmentFixture($fixtureId);
316+
317+
$this->assertSame(Response::HTTP_INTERNAL_SERVER_ERROR, $response->getStatusCode());
318+
$this->assertFalse($payload['success']);
319+
$this->assertSame('Failed to fetch attachments', $payload['error']['message']);
320+
$this->assertSame('ATTACHMENT_ERROR', $payload['error']['code']);
321+
}
258322
}

tests/phpMyFAQ/Controller/Api/BackupControllerWebTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@
1212
#[UsesNamespace('phpMyFAQ')]
1313
final class BackupControllerWebTest extends ControllerWebTestCase
1414
{
15+
public function testBackupEndpointReturnsUnauthorizedProblemWhenApiIsDisabled(): void
16+
{
17+
$this->getConfiguration('api')->getAll();
18+
$this->overrideConfigurationValues(['api.enableAccess' => false], 'api');
19+
20+
$response = $this->requestApi('GET', '/v3.2/backup/data');
21+
22+
self::assertResponseStatusCodeSame(401, $response);
23+
self::assertStringContainsString('problem+json', (string) $response->headers->get('Content-Type'));
24+
self::assertJson((string) $response->getContent());
25+
}
26+
1527
public function testBackupEndpointReturnsUnauthorizedWhenAnonymous(): void
1628
{
1729
$this->overrideConfigurationValues(['api.enableAccess' => true], 'api');

0 commit comments

Comments
 (0)