Skip to content
Open
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
10 changes: 8 additions & 2 deletions lib/WebPushClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,14 @@ private function getClient(): WebPush {
*/
private function getVapid(): array {
// Do not use lazy for now
$publicKey = $this->appConfig->getAppValueString('webpush_vapid_pubkey');
$privateKey = $this->appConfig->getAppValueString('webpush_vapid_privkey');
try {
$publicKey = $this->appConfig->getAppValueString('webpush_vapid_pubkey');
$privateKey = $this->appConfig->getAppValueString('webpush_vapid_privkey');
} catch (\Throwable) {
// Decryption failed (e.g. mismatched instance secret), regenerate keys
$publicKey = '';
$privateKey = '';
}
if ($publicKey === '' || $privateKey === '') {
/** @var array{publicKey: string, privateKey: string} $vapid */
$vapid = VAPID::createVapidKeys();
Expand Down
73 changes: 73 additions & 0 deletions tests/Unit/WebPushClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Notifications\Tests\Unit;

use OCA\Notifications\WebPushClient;
use OCP\AppFramework\Services\IAppConfig;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class WebPushClientTest extends TestCase {
protected IAppConfig&MockObject $appConfig;

protected function setUp(): void {
parent::setUp();
$this->appConfig = $this->createMock(IAppConfig::class);
}

public function testConstructSucceedsWhenVapidKeysAreStored(): void {
$this->appConfig->method('getAppValueString')
->willReturnMap([
['webpush_vapid_pubkey', '', false, 'BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw'],
['webpush_vapid_privkey', '', false, 'test-private-key'],
]);

$this->appConfig->expects($this->never())->method('setAppValueString');

$client = new WebPushClient($this->appConfig);
$this->assertInstanceOf(WebPushClient::class, $client);
}

public function testConstructRegeneratesVapidKeysWhenDecryptionFails(): void {
// Simulates the case where the stored VAPID keys were encrypted with a
// different instance secret — getAppValueString throws during decryption.
$this->appConfig->method('getAppValueString')
->willThrowException(new \RuntimeException('HMAC does not match.'));

$this->appConfig->expects($this->exactly(2))
->method('setAppValueString')
->with($this->logicalOr(
$this->equalTo('webpush_vapid_pubkey'),
$this->equalTo('webpush_vapid_privkey'),
));

// Must not throw — corrupted keys should be transparently regenerated
$client = new WebPushClient($this->appConfig);
$this->assertInstanceOf(WebPushClient::class, $client);
}

public function testConstructRegeneratesVapidKeysWhenMissing(): void {
$this->appConfig->method('getAppValueString')
->willReturnMap([
['webpush_vapid_pubkey', '', false, ''],
['webpush_vapid_privkey', '', false, ''],
]);

$this->appConfig->expects($this->exactly(2))
->method('setAppValueString')
->with($this->logicalOr(
$this->equalTo('webpush_vapid_pubkey'),
$this->equalTo('webpush_vapid_privkey'),
));

$client = new WebPushClient($this->appConfig);
$this->assertInstanceOf(WebPushClient::class, $client);
}
}
Loading