Skip to content

Commit 922bb02

Browse files
committed
test: added more tests
1 parent 7d753c7 commit 922bb02

26 files changed

Lines changed: 1574 additions & 39 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This is a log of major user-visible changes in each phpMyFAQ release.
2424
- added Symfony Kernel for better application structure and extensibility (Thorsten)
2525
- added optional Redis support for configuration caching (Thorsten)
2626
- added LDAP configuration frontend (Thorsten)
27+
- added phpMyFAQ recent news widget to the admin dashboard (Thorsten)
2728
- added experimental support for API key authentication via OAuth2 (Thorsten)
2829
- added experimental per-tenant quota enforcement, and API request rate limits (Thorsten)
2930
- improved audit and activity log with comprehensive security event tracking (Thorsten)

docs/administration.md

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
# 7. Manage phpMyFAQ
22

3-
The administration of phpMyFAQ is completely browser-based. The admin area can be found under this URL:
3+
The phpMyFAQ administration area is fully browser-based. You can access it at:
44

5-
`http://www.example.com/faq/admin/index.php`
5+
`https://www.example.com/faq/admin/index.php`
66

7-
You can also log in the public frontend, and after the successful login you'll see a link to administration backend,
8-
too.
7+
You can also log in through the public frontend. After a successful login, a link to the administration backend will be
8+
available there as well.
99

10-
If you've lost your password, you can reset it. A new random password will be generated and sent to you via email.
11-
Please change it after your successful login with the generated password.
10+
If you have lost your password, you can reset it. phpMyFAQ will generate a new random password and send it to you by
11+
email. After logging in with the generated password, you should change it immediately.
1212

13-
After entering your username and password, you can log into the system. On the dashboard page you can see the following
14-
cards:
13+
Once you enter your username and password, you can log in to the system. The dashboard provides several useful cards,
14+
including:
1515

16-
- some statistics about visits, entries, news, and comments
17-
- the latest current number fetched from phpmyfaq.de
18-
- a nice diagram with the number of visitors of the last 30 days
19-
- a list on inactive FAQs
20-
- a button to verify the integrity of the phpMyFAQ installation. Then clicking on the button phpMyFAQ calculates a
21-
SHA-1 hash for all files and checks it against a web service provided on phpmyfaq.de. With this service, it's possible
22-
to see if someone changed files.
16+
* statistics about visits, entries, news, and comments
17+
* the latest current version number fetched from phpmyfaq.de
18+
* a chart showing the number of visitors over the last 30 days
19+
* a list of inactive FAQs
20+
* a list of the latest user registrations
21+
* information about the date of the most recently verified backup
22+
* a button to verify the integrity of the phpMyFAQ installation
2323

24-
You can switch the current language in the administration backend, and you have an info box about the session timeout.
24+
When you click the integrity check button, phpMyFAQ calculates a SHA-1 hash for all files and compares it with a web
25+
service provided by phpmyfaq.de. This makes it possible to detect whether any files have been modified.
26+
27+
You can also switch the current language in the administration backend. In addition, an information box can display the
28+
session timeout if this feature is enabled.
2529

2630
## 5.1 Users and Groups
2731

@@ -81,7 +85,9 @@ To do this, enter the same file name for each entry in your **database, "faqcate
8185

8286
### 5.2.2 Add a new FAQ
8387

84-
You can create a completely new FAQ by using the 'Add new FAQ' option. When doing so, it is essential to select the desired category within the 'FAQ metadata' tab to ensure the entry is correctly indexed and visible to users. For a detailed explanation of all available settings and metadata options, please refer to section 5.2.3.
88+
You can create a completely new FAQ by using the 'Add new FAQ' option. When doing so, it is essential to select the
89+
desired category within the 'FAQ metadata' tab to ensure the entry is correctly indexed and visible to users. For a
90+
detailed explanation of all available settings and metadata options, please refer to section 5.2.3.
8591

8692
### 5.2.3 FAQ Administration
8793

@@ -188,31 +194,43 @@ Only the status will be changed - the FAQ will not be deleted from the database.
188194

189195
### 5.2.5 Orphaned FAQs
190196

191-
Orphaned FAQs are records that are no longer assigned to any category. This typically happens when categories are deleted without moving the associated FAQs, or through incomplete data imports. As they lack a category assignment, these entries are hidden from users in the public frontend.
197+
Orphaned FAQs are records that are no longer assigned to any category. This typically happens when categories are
198+
deleted without moving the associated FAQs, or through incomplete data imports. As they lack a category assignment,
199+
these entries are hidden from users in the public frontend.
192200

193201
Cross-Language Management
194-
Administrators can manage orphaned FAQs across all installed languages, without needing to switch their backend interface language. The list displays all orphaned entries in the system, including the full language name, and allows direct editing within the correct linguistic context.
202+
Administrators can manage orphaned FAQs across all installed languages, without needing to switch their backend
203+
interface language. The list displays all orphaned entries in the system, including the full language name, and allows
204+
direct editing within the correct linguistic context.
195205

196206
How to Resolve Orphaned FAQs:
197207
To correct an orphaned FAQ entry, you must assign it to a valid category in the corresponding language:
198208

199-
1. Open the FAQ: The orphaned FAQ list displays entries from all languages. Click on the desired entry to open the FAQ editor, which will automatically be set to the FAQ's language.
209+
1. Open the FAQ: The orphaned FAQ list displays entries from all languages. Click on the desired entry to open the FAQ
210+
editor, which will automatically be set to the FAQ's language.
200211

201-
2. Assign Category: Navigate to the "FAQ metadata" tab within the editor. The available categories in the dropdown list will automatically match the language of the FAQ you are editing, ensuring you can only select a category appropriate for that language.
212+
2. Assign Category: Navigate to the "FAQ metadata" tab within the editor. The available categories in the dropdown list
213+
will automatically match the language of the FAQ you are editing, ensuring you can only select a category appropriate
214+
for that language.
202215

203216
3. Save Changes: Select a valid category and save the FAQ.
204217

205218
Once saved, the entry will be visible in its new category and will automatically be removed from the Orphaned FAQ list.
206219

207220
### 5.2.6 Open Questions
208221

209-
On the "Open Questions" page, you can see all open questions that visitors have posted in the currently selected administration language.
210-
You can answer open questions directly or, if they are not visible in the public area due to visibility settings, you can activate them.
222+
On the "Open Questions" page, you can see all open questions that visitors have posted in the currently selected
223+
administration language.
224+
You can answer open questions directly or, if they are not visible in the public area due to visibility settings, you
225+
can activate them.
211226
Additionally, you can delete them.
212227

213228
Please note:
214-
Questions awaiting moderation in other languages are not automatically shown here. If questions exist in other active languages, a warning alert will be displayed at the bottom of the page indicating which languages have pending items and the respective counts.
215-
To process these other questions, you must change your current administration language using the language selector in the top menu.
229+
Questions awaiting moderation in other languages are not automatically shown here. If questions exist in other active
230+
languages, a warning alert will be displayed at the bottom of the page indicating which languages have pending items
231+
and the respective counts.
232+
To process these other questions, you must change your current administration language using the language selector in
233+
the top menu.
216234

217235
### 5.2.7 Comment Administration
218236

@@ -850,7 +868,8 @@ This page is only available if OpenSearch is enabled.
850868
### 5.6.8 System information
851869

852870
On this page, phpMyFAQ displays some relevant system information like PHP version, database version, or session path.
853-
Please use this information when reporting bugs.
871+
Please use this information when reporting bugs. Additionally, you can check the status of all translation files and see
872+
if there are any missing translations.
854873

855874
## 5.7 Using Microsoft Entra ID
856875

phpmyfaq/src/phpMyFAQ/Faq.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,11 +436,13 @@ public function renderFaqsByCategoryId(int $categoryId, string $orderBy = 'id',
436436
$link->getSEOTitle($title),
437437
);
438438

439+
$category = new Category($this->configuration);
440+
439441
$baseUrl = sprintf(
440-
'%sindex.php?%saction=show&cat=%d&seite=%d',
442+
'%scategory/%d/%s.html?seite=%d',
441443
$this->configuration->getDefaultUrl(),
442-
$sids === '' || $sids === '0' ? '' : $sids,
443444
$categoryId,
445+
TitleSlugifier::slug($category->getCategoryName($categoryId)),
444446
$page,
445447
);
446448

phpmyfaq/src/phpMyFAQ/Filesystem/Filesystem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public function copy(string $sourceFileName, string $destinationFileName): bool
150150

151151
if (copy($sourceFileName, $destinationFileName) === false) {
152152
$error = error_get_last();
153-
throw new Exception($error['message']);
153+
throw new Exception($error['message'] ?? 'Copy operation failed.');
154154
}
155155

156156
return true;

phpmyfaq/src/phpMyFAQ/Helper/CategoryHelper.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use phpMyFAQ\Core\Exception;
2626
use phpMyFAQ\Language\LanguageCodes;
2727
use phpMyFAQ\Link;
28+
use phpMyFAQ\Link\Util\TitleSlugifier;
2829
use phpMyFAQ\Strings;
2930
use phpMyFAQ\Translation;
3031
use phpMyFAQ\User;
@@ -95,14 +96,19 @@ public function buildCategoryList(
9596
break;
9697
}
9798

98-
$name = Strings::htmlentities($node['name']);
99+
$name = $node['name'];
99100
if ($categoryNumbers[$categoryId]['faqs'] > 0) {
100-
$url = sprintf('%sindex.php?action=show&cat=%d', $this->configuration->getDefaultUrl(), $node['id']);
101+
$url = sprintf(
102+
'%scategory/%d/%s.html',
103+
$this->configuration->getDefaultUrl(),
104+
$node['id'],
105+
TitleSlugifier::slug($name),
106+
);
101107

102108
$link = new Link($url, $this->configuration);
103-
$link->setTitle(Strings::htmlentities($node['name']));
104-
$link->text = Strings::htmlentities($node['name']);
105-
$link->tooltip = is_null($node['description']) ? '' : Strings::htmlentities($node['description']);
109+
$link->setTitle($node['name']);
110+
$link->text = $node['name'];
111+
$link->tooltip = is_null($node['description']) ? '' : $node['description'];
106112
$name = $link->toHtmlAnchor();
107113
}
108114

tests/phpMyFAQ/CustomPageTest.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,58 @@ public function testCountPages(): void
273273
$this->assertEquals(5, $count);
274274
}
275275

276+
public function testGetAllLanguagesPaginated(): void
277+
{
278+
$mockData = new stdClass();
279+
$mockData->id = 10;
280+
$mockData->lang = 'de';
281+
$mockData->page_title = 'All Languages Page';
282+
$mockData->slug = 'all-languages-page';
283+
$mockData->content = '<p>Paginated content</p>';
284+
$mockData->author_name = 'Author';
285+
$mockData->author_email = 'author@example.org';
286+
$mockData->active = 'y';
287+
$mockData->created = '2026-01-12 12:00:00';
288+
$mockData->updated = null;
289+
$mockData->seo_title = null;
290+
$mockData->seo_description = null;
291+
$mockData->seo_robots = 'index,follow';
292+
293+
$this->mockRepository
294+
->expects($this->once())
295+
->method('getAllLanguagesPaginated')
296+
->with(false, 25, 0, 'created', 'DESC')
297+
->willReturn([$mockData]);
298+
299+
$result = $this->customPage->getAllLanguagesPaginated();
300+
301+
$this->assertCount(1, $result);
302+
$this->assertSame('All Languages Page', $result[0]['page_title']);
303+
$this->assertSame('de', $result[0]['lang']);
304+
}
305+
306+
public function testCountAllLanguages(): void
307+
{
308+
$this->mockRepository
309+
->expects($this->once())
310+
->method('countAllLanguages')
311+
->with(true)
312+
->willReturn(3);
313+
314+
$this->assertSame(3, $this->customPage->countAllLanguages(true));
315+
}
316+
317+
public function testGetExistingLanguages(): void
318+
{
319+
$this->mockRepository
320+
->expects($this->once())
321+
->method('getExistingLanguages')
322+
->with(23)
323+
->willReturn(['de', 'en', 'fr']);
324+
325+
$this->assertSame(['de', 'en', 'fr'], $this->customPage->getExistingLanguages(23));
326+
}
327+
276328
public function testSlugExists(): void
277329
{
278330
$this->mockRepository
@@ -326,6 +378,28 @@ public function testGenerateUniqueSlugWithExcludeId(): void
326378
$this->assertEquals('test-slug', $uniqueSlug);
327379
}
328380

381+
public function testCreateTranslation(): void
382+
{
383+
$page = new CustomPageEntity();
384+
$page
385+
->setLanguage('de')
386+
->setPageTitle('Translated Page')
387+
->setSlug('translated-page')
388+
->setContent('<p>Translated content</p>')
389+
->setAuthorName('Test Author')
390+
->setAuthorEmail('test@example.org')
391+
->setActive(true)
392+
->setCreated(new DateTime());
393+
394+
$this->mockRepository
395+
->expects($this->once())
396+
->method('insertTranslation')
397+
->with($page, 11)
398+
->willReturn(true);
399+
400+
$this->assertTrue($this->customPage->createTranslation($page, 11));
401+
}
402+
329403
public function testGetByIdWithSeoFields(): void
330404
{
331405
$mockData = new stdClass();
@@ -355,4 +429,34 @@ public function testGetByIdWithSeoFields(): void
355429
$this->assertEquals('SEO Test Description', $result->getSeoDescription());
356430
$this->assertEquals('noindex,nofollow', $result->getSeoRobots());
357431
}
432+
433+
public function testGetByIdMapsUpdatedTimestamp(): void
434+
{
435+
$mockData = new stdClass();
436+
$mockData->id = 2;
437+
$mockData->lang = 'en';
438+
$mockData->page_title = 'Updated Page';
439+
$mockData->slug = 'updated-page';
440+
$mockData->content = '<p>Updated content</p>';
441+
$mockData->author_name = 'Test Author';
442+
$mockData->author_email = 'test@example.org';
443+
$mockData->active = 'y';
444+
$mockData->created = '2026-01-12 12:00:00';
445+
$mockData->updated = '2026-01-13 09:30:00';
446+
$mockData->seo_title = null;
447+
$mockData->seo_description = null;
448+
$mockData->seo_robots = 'index,follow';
449+
450+
$this->mockRepository
451+
->expects($this->once())
452+
->method('getById')
453+
->with(2, 'en')
454+
->willReturn($mockData);
455+
456+
$result = $this->customPage->getById(2);
457+
458+
$this->assertInstanceOf(CustomPageEntity::class, $result);
459+
$this->assertNotNull($result->getUpdated());
460+
$this->assertSame('2026-01-13 09:30:00', $result->getUpdated()?->format('Y-m-d H:i:s'));
461+
}
358462
}

tests/phpMyFAQ/Faq/QuestionServiceTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class QuestionServiceTest extends TestCase
2323
private CurrentUser|MockObject $currentUser;
2424
private array $currentGroups;
2525
private QuestionService $questionService;
26+
private string $databaseFile;
2627

2728
/**
2829
* @throws Exception
@@ -40,9 +41,12 @@ protected function setUp(): void
4041
->setCurrentLanguage('en')
4142
->setMultiByteLanguage();
4243

44+
$this->databaseFile = PMF_TEST_DIR . '/question-service-' . uniqid('', true) . '.db';
45+
copy(PMF_TEST_DIR . '/test.db', $this->databaseFile);
46+
4347
// Create configuration with real database
4448
$dbHandle = new Sqlite3();
45-
$dbHandle->connect(PMF_TEST_DIR . '/test.db', '', '');
49+
$dbHandle->connect($this->databaseFile, '', '');
4650
$this->configuration = new Configuration($dbHandle);
4751

4852
$language = new Language($this->configuration, $this->createStub(Session::class));
@@ -60,6 +64,12 @@ protected function setUp(): void
6064
$this->questionService = new QuestionService($this->configuration, $this->currentUser, $this->currentGroups);
6165
}
6266

67+
protected function tearDown(): void
68+
{
69+
parent::tearDown();
70+
@unlink($this->databaseFile);
71+
}
72+
6373
/**
6474
* @throws Exception
6575
*/

0 commit comments

Comments
 (0)