Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions core/recenttopics.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,23 @@ public function display_recent_topics($tpl_loopname = 'recent_topics', $context
$this->excluded_topics = explode(',', $this->config['rt_anti_topics']);
$min_topic_level = $this->config['rt_min_topic_level'];

$this->sort_topics = $this->config['rt_sort_start_time'] ? 'topic_time' : 'topic_last_post_time';
// if user can set recent topic sorting order and it is set then use the preference
if ($this->auth->acl_get('u_rt_sort_start_time') && isset($this->user->data['user_rt_sort_start_time']))
{
$this->sort_topics = $this->user->data['user_rt_sort_start_time'] ? 'topic_time' : 'topic_last_post_time';
}

$this->get_forum_list();
// No forums to display
if (count($this->forum_ids) == 0)
{
return;
}

//limit number of pages to be shown
// compute as product of topics per page and max number of pages.
// The count needs the forum list above, or it only counts m_approve forums (issue #195).
$this->total_topics_limit = 0;
if ((int) $this->config['rt_page_number'] == 0)
{
Expand All @@ -352,20 +367,6 @@ public function display_recent_topics($tpl_loopname = 'recent_topics', $context

}

$this->sort_topics = $this->config['rt_sort_start_time'] ? 'topic_time' : 'topic_last_post_time';
// if user can set recent topic sorting order and it is set then use the preference
if ($this->auth->acl_get('u_rt_sort_start_time') && isset($this->user->data['user_rt_sort_start_time']))
{
$this->sort_topics = $this->user->data['user_rt_sort_start_time'] ? 'topic_time' : 'topic_last_post_time';
}

$this->get_forum_list();
// No forums to display
if (count($this->forum_ids) == 0)
{
return;
}

$topics_count = $this->get_topic_list();

if (count($this->topic_list) == 0)
Expand Down
7 changes: 4 additions & 3 deletions event/ucp_listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public static function getSubscribedEvents()
return array(
'core.ucp_prefs_view_data' => 'ucp_prefs_get_data',
'core.ucp_prefs_view_update_data' => 'ucp_prefs_set_data',
'core.ucp_register_data_after' => 'ucp_register_set_data'
'core.ucp_register_register_after' => 'ucp_register_set_data'
);
}

Expand Down Expand Up @@ -277,8 +277,9 @@ public function ucp_prefs_set_data($event)
/**
* set a newly registered account's Recent Topics preferences from default.
*
* Fired after the user row has been inserted, so the user_rt_* columns are written by a second
* UPDATE of our own rather than merged into an existing $sql_ary.
* Listens to core.ucp_register_register_after, which fires after user_add() has inserted the row
* and carries the new user_id, so the user_rt_* columns are written by a second UPDATE of our own.
* core.ucp_register_data_after fires during form validation with no user_id (issue #196).
*
* @param \phpbb\event\data $event Event object; reads ['user_id']
* @return void
Expand Down
118 changes: 118 additions & 0 deletions tests/core/topic_count_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
*
* @package Recent Topics Extension
* @copyright (c) 2026 Andreas Vandenberghe
* @license GNU General Public License, version 2 (GPL-2.0)
*
* Tests for the "Show all recent topic pages" count in
* recenttopics::display_recent_topics().
*
* See: https://github.com/avatharbe/RecentTopics/issues/195
*/

// display_recent_topics() includes functions_display.php unless topic_status()
// already exists; the stub keeps the test independent of a board install.
namespace
{
if (!function_exists('topic_status'))
{
function topic_status($row, $replies, $unread_topic, &$folder_img, &$folder_alt, &$topic_type)
{
$folder_img = 'folder';
$folder_alt = 'TOPIC_READ';
$topic_type = '';
}
}
}

namespace avathar\recenttopics\tests\core
{

class topic_count_test extends \phpbb_test_case
{
/**
* With "Show all recent topic pages" on, every topic query, including the
* count that sets the page limit, must be scoped to the user's forum list
* (#195). Before the fix the count ran before get_forum_list() and got null,
* so it only counted forums where the user has m_approve.
*/
public function test_page_count_query_uses_forum_list()
{
$visibility_forum_ids = [];
$content_visibility = $this->createMock(\phpbb\content_visibility::class);
$content_visibility->method('get_forums_visibility_sql')
->willReturnCallback(function ($mode, $forum_ids) use (&$visibility_forum_ids) {
$visibility_forum_ids[] = $forum_ids;
return '1=1';
});

$auth = $this->createMock(\phpbb\auth\auth::class);
$auth->method('acl_get')->willReturnCallback(function ($opt) {
return $opt === 'u_rt_view';
});
$auth->method('acl_getf')->willReturnCallback(function ($opt) {
return $opt === 'f_read' ? [1 => ['f_read' => 1], 2 => ['f_read' => 1]] : [];
});

$user = $this->createMock(\phpbb\user::class);
$user->data = ['user_id' => 2, 'user_rt_enable' => 1, 'is_registered' => true];
$user->method('get_passworded_forums')->willReturn([]);

$queried = [];
$db = $this->createMock(\phpbb\db\driver\driver_interface::class);
$db->method('sql_in_set')->willReturnCallback(function ($field, $ids) use (&$queried) {
if ($field === 'forum_id')
{
$queried = array_values($ids);
}
return '1=1';
});
$db->method('sql_build_query')->willReturn('SELECT 1');
$db->method('sql_query')->willReturn('result');
$db->method('sql_query_limit')->willReturn('result');
$db->method('sql_fetchfield')->willReturn(0);
$db->method('sql_fetchrow')->willReturnCallback(function () use (&$queried) {
$forum_id = array_shift($queried);
return $forum_id === null ? false : ['forum_id' => $forum_id];
});

$rt = new \avathar\recenttopics\core\recenttopics(
$auth,
$this->createMock(\phpbb\cache\service::class),
new \phpbb\config\config([
'rt_parents' => 0,
'rt_location' => 'RT_TOP',
'rt_unread_only' => 0,
'rt_number' => 5,
'rt_anti_topics' => '0',
'rt_min_topic_level' => 0,
'rt_page_number' => 1,
'rt_page_numbermax' => 0,
'rt_sort_start_time' => 0,
]),
$this->createMock(\phpbb\language\language::class),
$content_visibility,
$db,
new \phpbb\event\dispatcher(),
$this->createMock(\phpbb\pagination::class),
$this->createMock(\phpbb\request\request_interface::class),
$this->createMock(\phpbb\template\template::class),
$user,
'/',
'php',
$this->createMock(\phpbb\config\db_text::class)
);

$rt->display_recent_topics();

$this->assertNotEmpty($visibility_forum_ids, 'no topic query was built');
foreach ($visibility_forum_ids as $forum_ids)
{
$forum_ids = array_values((array) $forum_ids);
sort($forum_ids);
$this->assertSame([1, 2], $forum_ids);
}
}
}
}
18 changes: 16 additions & 2 deletions tests/event/ucp_listener_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,23 @@ public function test_getSubscribedEvents()
$this->assertEquals(array(
'core.ucp_prefs_view_data',
'core.ucp_prefs_view_update_data',
'core.ucp_register_data_after',
'core.ucp_register_register_after',
), array_keys(\avathar\recenttopics\event\ucp_listener::getSubscribedEvents()));
}

/**
* The registration defaults must hang off an event that fires after user_add() and
* carries the new user_id. core.ucp_register_data_after fires during form validation,
* has no user_id, and made the UPDATE hit user_id = 0 (#196).
*/
public function test_register_defaults_run_after_the_account_exists()
{
$events = \avathar\recenttopics\event\ucp_listener::getSubscribedEvents();

$this->assertSame('ucp_register_set_data', $events['core.ucp_register_register_after'] ?? null);
$this->assertArrayNotHasKey('core.ucp_register_data_after', $events);
}

/**
* A user holding every RT permission has all six preferences persisted.
*/
Expand Down Expand Up @@ -273,7 +286,8 @@ public function test_ucp_register_set_data()
->willReturn("user_rt_enable = 1");

$this->db->expects($this->once())
->method('sql_query');
->method('sql_query')
->with($this->stringContains('WHERE user_id = 3'));

$this->set_listener();

Expand Down
22 changes: 19 additions & 3 deletions tests/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ tests/
β”‚ └── page_controller_test.php Dedicated /rt and /rt/simple page controller
β”œβ”€β”€ core/
β”‚ β”œβ”€β”€ forum_list_test.php Which forums the list may draw topics from
β”‚ β”œβ”€β”€ topic_count_test.php "Show all pages" count uses the forum list
β”‚ └── recenttopics_events_test.php Public event API contract (contrib/events.md)
└── functional/
└── recenttopics_test.php End-to-end browser tests
Expand Down Expand Up @@ -88,7 +89,7 @@ It creates the listener with mocked dependencies β€” a `config` object with real
There are three handlers:
- `ucp_prefs_get_data` β€” runs on page load AND on form submit; builds the data array and (only on page load) renders the UCP template block
- `ucp_prefs_set_data` β€” maps the submitted form fields to the SQL column names used in `phpbb_users`
- `ucp_register_set_data` β€” runs when a new account is created and writes the global config defaults to that user's row
- `ucp_register_set_data` β€” runs on `core.ucp_register_register_after`, after the new account is inserted, and writes the global config defaults to that user's row

**What this test file does:**
Creates the listener with mocks for auth, config, request, template, user, language, and db. Because `ucp_register_set_data` actually runs a database query, it is the only handler tested with mock expectations on `sql_build_array` and `sql_query`.
Expand All @@ -99,7 +100,8 @@ Creates the listener with mocks for auth, config, request, template, user, langu
| `test_ucp_prefs_set_data` | Submits 5 preference fields | Each `data['rt_*']` field must map to the correct `sql_ary['user_rt_*']` column; a mismatch means preferences silently fail to save |
| `test_ucp_prefs_get_data_no_submit` | Page load (submit = false) | Must: merge user DB values into `data`, call `add_lang()`, call `template->assign_vars()` |
| `test_ucp_prefs_get_data_on_submit` | Form submit (submit = true) | Must: merge data; must NOT call `template->assign_vars()` β€” template must only be touched on page load, not on form processing |
| `test_ucp_register_set_data` | New user registration (user_id = 3) | Must call `sql_build_array('UPDATE', ...)` with all 5 default values, then `sql_query()` β€” verified by mock expectations on the db object |
| `test_register_defaults_run_after_the_account_exists` | Event map | `ucp_register_set_data` must be on `core.ucp_register_register_after` (fires after `user_add()`, carries `user_id`) and not on `core.ucp_register_data_after` (fires during validation, no `user_id`, so the UPDATE hit `user_id = 0`; #196) |
| `test_ucp_register_set_data` | New user registration (user_id = 3) | Must call `sql_build_array('UPDATE', ...)` with all 5 default values, then `sql_query()` with `WHERE user_id = 3` β€” verified by mock expectations on the db object |

---

Expand Down Expand Up @@ -174,7 +176,21 @@ It builds `core\recenttopics` with a mocked `auth` (returning the readable forum

---

## 6. Functional tests (`functional/recenttopics_test.php`)
## 6. Page count (`core/topic_count_test.php`)

**What this code does:**
With "Show all recent topic pages" (`rt_page_number`) on, `display_recent_topics()` counts the user's recent topics to set the page limit. The count must use the same forum list as the topic list itself.

**What this test file does:**
It runs `display_recent_topics()` with mocked `auth`, `user`, `db` and `content_visibility`, a real event dispatcher, and `rt_page_number = 1`. The `content_visibility` mock records the forum ids each topic query is scoped to.

| Test | Scenario | What it verifies |
|------|----------|-----------------|
| `test_page_count_query_uses_forum_list` | Readable forums 1 and 2; "Show all pages" on | Every topic query, including the page count, is scoped to forums 1 and 2. Before the fix the count ran before `get_forum_list()` and got `null`, so it only counted forums where the user has `m_approve` (#195) |

---

## 7. Functional tests (`functional/recenttopics_test.php`)

**What this code does:**
These tests start a real phpBB installation (using the test framework's built-in install), enable the extension, and make HTTP requests using a real browser-like crawler (Symfony DomCrawler). They check the actual rendered HTML for specific elements.
Expand Down
Loading