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
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
uses: phpbb-extensions/test-framework/.github/workflows/tests.yml@3.3.x
with:
EXTNAME: phpbb/webpushnotifications # Your extension vendor/package name
EXECUTABLE_FILES: 0
RUN_NPM_INSTALL: 1
RUN_COMPOSER_INSTALL: 1
PHP_VERSION_MATRIX: '["7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
This is the repository for the development of the phpBB Browser Push Notifications extension.

[![Build Status](https://github.com/phpbb-extensions/webpushnotifications/actions/workflows/tests.yml/badge.svg)](https://github.com/phpbb-extensions/webpushnotifications/actions)
[![codecov](https://codecov.io/gh/phpbb-extensions/webpushnotifications/graph/badge.svg?token=TKEZM2VFKI)](https://codecov.io/gh/phpbb-extensions/webpushnotifications)

An official phpBB extension that allows board users to receive browser-based push notifications.

Expand Down
6 changes: 3 additions & 3 deletions language/en/webpushnotifications_faq.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
'HELP_FAQ_WPN_WHAT_QUESTION' => 'What are web push notifications?',
'HELP_FAQ_WPN_WHAT_ANSWER' => 'Web push notifications enhance phpBB’s notification system by allowing real-time notifications to be sent directly to your desktop or mobile device, even if you’re not actively browsing the forum. These notifications function like app alerts, providing instant updates for private messages, post interactions, moderation actions, and more.',
'HELP_FAQ_WPN_HOW_QUESTION' => 'How can I receive forum notification alerts on my computer or mobile device?',
'HELP_FAQ_WPN_HOW_ANSWER' => 'Navigate to “Notification options” in your UCP (User Control Panel) and click “Enable web push notifications.” Your browser may ask for permission to send notifications—be sure to allow it. If you’re still not receiving notifications, check your device’s system settings to ensure notifications are enabled for your browser. For mobile devices such as iPhone or iPad, you may need to add the forum site to your Home Screen for push notifications to work, effectively turning it into a standalone web app. Follow your mobile device’s instructions to enable push notifications for <a href="https://www.xda-developers.com/how-enable-safari-notifications-iphone/" target="_blank">iPhone/iPad</a> or <a href="https://support.google.com/chrome/answer/3220216?hl=en&co=GENIE.Platform%3DAndroid&oco=0" target="_blank">Android</a>.',
'HELP_FAQ_WPN_HOW_ANSWER' => 'Navigate to “Notification options” in your UCP (User Control Panel) and click “Enable push notifications.” Your browser may ask for permission to send notifications—be sure to allow it. If you’re still not receiving notifications, check your device’s system settings to ensure notifications are enabled for your browser. For mobile devices such as iPhone or iPad, you may need to add the forum site to your Home Screen for push notifications to work, effectively turning it into a standalone web app. Follow your mobile device’s instructions to enable push notifications for <a href="https://www.xda-developers.com/how-enable-safari-notifications-iphone/" target="_blank">iPhone/iPad</a> or <a href="https://support.google.com/chrome/answer/3220216?hl=en&co=GENIE.Platform%3DAndroid&oco=0" target="_blank">Android</a>.',
'HELP_FAQ_WPN_SESSION_QUESTION' => 'Will I receive notifications if I am logged out?',
'HELP_FAQ_WPN_SESSION_ANSWER' => 'Yes, you will continue to receive notifications even if you’re logged out.',
'HELP_FAQ_WPN_SUBBING_QUESTION' => 'Why are the “Enable Push Notifications” buttons disabled?',
'HELP_FAQ_WPN_SUBBING_ANSWER' => 'If the “Enable Push Notifications” buttons is visible but cannot be clicked, your browser or device likely doesn’t support push notifications. Try using a different browser or device that supports this feature.',
'HELP_FAQ_WPN_SUBBING_QUESTION' => 'Why are the “Enable push notifications” buttons disabled?',
'HELP_FAQ_WPN_SUBBING_ANSWER' => 'If the “Enable push notifications” buttons is visible but cannot be clicked, your browser or device likely doesn’t support push notifications. Try using a different browser or device that supports this feature.',
'HELP_FAQ_WPN_GENERAL_QUESTION' => 'What if I’m still having trouble receiving notifications?',
'HELP_FAQ_WPN_GENERAL_ANSWER' => 'Make sure this forum is allowed to send notifications in your browser settings. Also, verify that your device’s system settings permit notifications from your web browser or app. Some browsers deliver notifications even when closed, whilst others only do so when the browser is open. <a href="https://caniuse.com/push-api" target="_blank">View this table for browser support information.</a> Finally, if you’re using an ad blocker, review its settings to make sure it’s not configured to block push notifications.',
]);
98 changes: 88 additions & 10 deletions tests/acp/acp_module_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,31 @@ public function test_main_rejects_invalid_form_key($mode): void
->with('submit')
->willReturn(true);

$this->setExpectedTriggerError(E_USER_WARNING, 'FORM_INVALID');
$this->assertTriggerError(E_USER_WARNING, 'FORM_INVALID', function() use ($mode) {
$this->create_module('adm.php?i=test&mode=' . $mode)->main('', $mode);
});
}

public function valid_submit_mode_data(): array
{
return [
'webpush' => ['webpush', 'save_settings'],
'pwa' => ['pwa', 'save_pwa_settings'],
];
}

/**
* @dataProvider valid_submit_mode_data
*/
public function test_main_routes_valid_submission($mode, $expected_method): void
{
$this->request->method('is_set_post')->with('submit')->willReturn(true);
$module = new \phpbb\webpushnotifications\acp\routing_acp_module();
$module->u_action = 'adm.php?i=test&mode=' . $mode;

$module->main('', $mode);

$this->create_module('adm.php?i=test&mode=' . $mode)->main('', $mode);
self::assertSame([$expected_method], $module->calls);
}

public function webpush_save_data(): array
Expand Down Expand Up @@ -402,10 +424,14 @@ public function test_save_settings_validates_and_persists_webpush_config(array $

if ($expect_saved)
{
$this->setExpectedTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED');
$this->assertTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED', function() {
$this->create_module()->save_settings();
});
}
else
{
$this->create_module()->save_settings();
}

$this->create_module()->save_settings();

foreach ($expected_config as $name => $value)
{
Expand Down Expand Up @@ -477,8 +503,8 @@ public function pwa_save_data(): array
],
[
[
'pwa_bg_color' => '#fff000',
'pwa_theme_color' => '#000fff',
'pwa_bg_color' => '',
'pwa_theme_color' => '',
],
[
'pwa_bg_color' => '',
Expand Down Expand Up @@ -643,10 +669,14 @@ public function test_save_pwa_settings_validates_and_persists_pwa_config(array $

if ($expect_saved)
{
$this->setExpectedTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED');
$this->assertTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED', function() {
$this->create_module('adm.php?i=test&mode=pwa')->save_pwa_settings();
});
}
else
{
$this->create_module('adm.php?i=test&mode=pwa')->save_pwa_settings();
}

$this->create_module('adm.php?i=test&mode=pwa')->save_pwa_settings();

foreach ($expected_config as $name => $value)
{
Expand Down Expand Up @@ -738,6 +768,31 @@ protected function set_protected_property($object, $property, $value): void
$property->setValue($object, $value);
}

protected function assertTriggerError($errno, $message, callable $callback): void
{
$caught = null;
set_error_handler(static function($severity, $error_message, $file, $line) {
throw new \ErrorException($error_message, 0, $severity, $file, $line);
}, $errno);

try
{
$callback();
}
catch (\ErrorException $exception)
{
$caught = $exception;
}
finally
{
restore_error_handler();
}

self::assertNotNull($caught, 'Expected trigger_error() was not raised.');
self::assertSame($errno, $caught->getSeverity());
self::assertStringContainsString($message, $caught->getMessage());
}

protected function get_protected_property($object, $property)
{
$reflection = new \ReflectionClass($object);
Expand Down Expand Up @@ -826,3 +881,26 @@ function check_form_key()
{
return \phpbb\webpushnotifications\tests\acp\acp_module_test::$valid_form;
}

class routing_acp_module extends wpn_acp_module
{
public $calls = [];

public function save_settings()
{
$this->calls[] = __FUNCTION__;
}

public function save_pwa_settings()
{
$this->calls[] = __FUNCTION__;
}

public function display_settings()
{
}

public function display_pwa_settings()
{
}
}
77 changes: 77 additions & 0 deletions tests/controller/controller_webpush_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,83 @@ public function test_is_valid_endpoint(string $endpoint, bool $expected): void
$this->assertEquals($expected, $this->controller->is_valid_endpoint($endpoint));
}

public function test_current_notification_data_is_rendered(): void
{
$notification = $this->createMock(\phpbb\notification\type\type_interface::class);
$notification->method('users_to_query')->willReturn([]);
$notification->method('get_title')->willReturn('A &amp; <b>title</b>');
$notification->method('get_reference')->willReturn('A &quot;reference&quot;');
$notification->method('get_url')->willReturn('viewtopic.php?p=1&amp;x=2');
$notification->method('get_avatar')->willReturn('<img src="phpBB/images/avatar.png" alt="">');
$this->notification_manager->expects(self::once())
->method('get_item_type_class')
->with('notification.type.test', self::isType('array'))
->willReturn($notification);

$method = new ReflectionMethod($this->controller, 'get_notification_data');
$method->setAccessible(true);
$data = json_decode($method->invoke($this->controller, json_encode([
'notification_type_name' => 'notification.type.test',
])), true);

self::assertSame('yourdomain.com', $data['heading']);
self::assertSame('A & title', $data['title']);
self::assertSame('A &quot;reference&quot;', $data['text']);
self::assertSame('viewtopic.php?p=1&x=2', $data['url']);
self::assertStringEndsWith('/images/avatar.png', $data['avatar']['src']);
}

public function subscription_write_data(): array
{
return [
'legacy seconds' => [['expiration_time' => 42, 'keys' => ['p256dh' => 'key', 'auth' => 'auth']], 42],
'empty browser timestamp' => [['expirationTime' => null, 'keys' => ['p256dh' => 'key', 'auth' => 'auth']], 0],
'browser milliseconds' => [['expirationTime' => 42000, 'keys' => ['p256dh' => 'key', 'auth' => 'auth']], 42],
];
}

/**
* @dataProvider subscription_write_data
*/
public function test_subscription_write_data_normalizes_expiration(array $data, $expected): void
{
$method = new ReflectionMethod($this->controller, 'get_subscription_write_data');
$method->setAccessible(true);

self::assertSame($expected, $method->invoke($this->controller, $data)['expiration_time']);
}

public function test_subscription_write_data_rejects_non_array_keys(): void
{
$method = new ReflectionMethod($this->controller, 'get_subscription_write_data');
$method->setAccessible(true);

$this->expectException(http_exception::class);
$this->expectExceptionMessage('AJAX_ERROR_TEXT');
$method->invoke($this->controller, ['keys' => 'invalid']);
}

public function avatar_data(): array
{
return [
'empty' => ['', ''],
'plain url' => ['https://example.com/avatar.png', 'https://example.com/avatar.png'],
'multiple sources use final source' => ['<img src="phpBB/fallback.png" data-src="phpBB/avatar.png">', 'avatar.png'],
];
}

/**
* @dataProvider avatar_data
*/
public function test_prepare_avatar($avatar, $expected_suffix): void
{
$method = new ReflectionMethod($this->controller, 'prepare_avatar');
$method->setAccessible(true);
$actual = $method->invoke($this->controller, $avatar);

self::assertStringEndsWith($expected_suffix, $actual['src']);
}

public function test_toggle_popup_enable_to_disable()
{
$this->form_helper->method('check_form_tokens')->willReturn(true);
Expand Down
34 changes: 32 additions & 2 deletions tests/event/listener_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ protected function setUp(): void
$form_request->enable_super_globals();
$this->request = $this->getMockBuilder('\phpbb\request\request_interface')
->getMock();
$this->request->method('server')
->willReturn('');
$user = new \phpbb\user($this->language, '\phpbb\datetime');
$this->user = $user;
$this->user->data['user_form_salt'] = '';
Expand Down Expand Up @@ -359,6 +357,38 @@ public function test_pwa_manifest()
$dispatcher->trigger_event('core.acp_main_notice');
}

public function test_load_template_data_returns_when_notifications_unavailable()
{
$this->config['wpn_webpush_enable'] = false;
$this->user->data['user_id'] = ANONYMOUS;
$this->notifications->expects(self::never())->method('get_subscription_methods');

$this->set_listener();
$this->listener->load_template_data();
}

public function test_pwa_manifest_builds_fallback_name_and_mobile_banner()
{
$this->config['sitename'] = 'Long &amp; Mobile Site';
$this->user->style = [
'pwa_theme_color' => '#112233',
'pwa_bg_color' => '#445566',
];
$this->request->method('server')->with('HTTP_USER_AGENT', '')->willReturn('Mozilla/5.0 iPhone Mobile');

$this->template->expects(self::once())->method('assign_vars')->with([
'U_MANIFEST_URL' => $this->controller_helper->route('phpbb_webpushnotifications_manifest_controller'),
'U_TOUCH_ICON' => null,
'SHORT_SITE_NAME' => 'Long &amp; Mobil',
'PWA_THEME_COLOR' => '#112233',
'PWA_BG_COLOR' => '#445566',
'S_PWA_SHOW_BANNER' => true,
]);

$this->set_listener();
$this->listener->pwa_manifest();
}

public function test_wpn_faq()
{
$this->language->add_lang('webpushnotifications_faq', 'phpbb/webpushnotifications');
Expand Down
85 changes: 85 additions & 0 deletions tests/form/form_helper_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php
/**
*
* phpBB Browser Push Notifications. An extension for the phpBB Forum Software package.
*
* @copyright (c) 2026, phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
*/

namespace phpbb\webpushnotifications\tests\form;

class form_helper_test extends \phpbb_test_case
{
protected $config;
protected $request;
protected $user;

protected function setUp(): void
{
parent::setUp();

$this->config = new \phpbb\config\config([
'form_token_lifetime' => 60,
'form_token_sid_guests' => true,
]);
$this->request = $this->createMock(\phpbb\request\request_interface::class);
$this->user = $this->createMock(\phpbb\user::class);
$this->user->data = [
'user_id' => 2,
'user_form_salt' => 'salt',
];
$this->user->session_id = 'session';
}

public function check_data(): array
{
return [
'valid user token' => [2, 60, null, 10, true, true, true],
'valid guest token with sid' => [ANONYMOUS, 60, null, 10, true, true, true],
'unlimited lifetime' => [2, -1, null, 3600, true, true, true],
'minimum lifetime enforced' => [2, 1, null, 31, true, true, false],
'explicit lifetime' => [2, 60, 5, 10, true, true, false],
'missing creation time' => [2, 60, null, 10, false, true, false],
'missing token' => [2, 60, null, 10, true, false, false],
'invalid token' => [2, 60, null, 10, true, true, false, 'invalid'],
];
}

/**
* @dataProvider check_data
*/
public function test_check_form_tokens($user_id, $lifetime, $timespan, $age, $has_time, $has_token, $expected, $token_override = null): void
{
$this->config['form_token_lifetime'] = $lifetime;
$this->user->data['user_id'] = $user_id;
$creation_time = time() - $age;
$token_sid = $user_id === ANONYMOUS ? $this->user->session_id : '';
$token = $token_override ?? sha1($creation_time . 'salt' . 'test-form' . $token_sid);

$this->request->method('is_set_post')->willReturnMap([
['creation_time', $has_time],
['form_token', $has_token],
]);
$this->request->method('variable')->willReturnMap([
['creation_time', 0, false, \phpbb\request\request_interface::REQUEST, $creation_time],
['form_token', '', false, \phpbb\request\request_interface::REQUEST, $token],
]);

$helper = new \phpbb\webpushnotifications\form\form_helper($this->config, $this->request, $this->user);
self::assertSame($expected, $helper->check_form_tokens('test-form', $timespan));
}

public function test_get_form_tokens_uses_guest_session(): void
{
$this->user->data['user_id'] = ANONYMOUS;
$helper = new \phpbb\webpushnotifications\form\form_helper($this->config, $this->request, $this->user);

$tokens = $helper->get_form_tokens('test-form', $now, $token_sid, $token);

self::assertSame('session', $token_sid);
self::assertSame(sha1($now . 'salt' . 'test-form' . 'session'), $token);
self::assertSame(['creation_time' => $now, 'form_token' => $token], $tokens);
}
}
Loading