untested... test: add unit test suite and CI workflow
I have never done this style of test before... who knows if it will work - Add Gitea Actions workflow for PHP 8.2 testing - Add PHPUnit configuration and bootstrap file - Add unit tests for TalkService, WebhookController, ImageCleanup, NavigationProvider, and DebugToggle - Add README documentation for the test suite - Update composer.json with test and lint scripts
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace OCA\Ncbotwebhooks\Test;
|
||||
|
||||
use OCA\Ncbotwebhooks\Command\DebugToggle;
|
||||
use OCP\AppConfig;
|
||||
use OCP\IAppConfig;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Unit tests for DebugToggle OCC command.
|
||||
*
|
||||
* Tests --status, --enable, --disable, toggle, and --enable/--disable conflict.
|
||||
*/
|
||||
class DebugToggleTest extends TestCase {
|
||||
private const APP_ID = 'nc_bot_webhooks';
|
||||
private const DEBUG_KEY = 'debug_enabled';
|
||||
|
||||
private function makeCommand(IAppConfig $appConfig): DebugToggle {
|
||||
return new DebugToggle($appConfig);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// --status
|
||||
// =========================================================================
|
||||
|
||||
public function testStatusReturnsDisabledByDefault(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(false);
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput(['--status' => true]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $result);
|
||||
$this->assertStringContainsString('DISABLED', $output->fetch());
|
||||
}
|
||||
|
||||
public function testStatusReturnsEnabledWhenSet(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(true);
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput(['--status' => true]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $result);
|
||||
$this->assertStringContainsString('ENABLED', $output->fetch());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// --enable
|
||||
// =========================================================================
|
||||
|
||||
public function testEnableSetsValueToTrue(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(false);
|
||||
$appConfig->expects($this->once())
|
||||
->method('setValueBool')
|
||||
->with(self::APP_ID, self::DEBUG_KEY, true);
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput(['--enable' => true]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $result);
|
||||
$this->assertStringContainsString('ENABLED', $output->fetch());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// --disable
|
||||
// =========================================================================
|
||||
|
||||
public function testDisableSetsValueToFalse(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(true);
|
||||
$appConfig->expects($this->once())
|
||||
->method('setValueBool')
|
||||
->with(self::APP_ID, self::DEBUG_KEY, false);
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput(['--disable' => true]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $result);
|
||||
$this->assertStringContainsString('DISABLED', $output->fetch());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Toggle (no flags)
|
||||
// =========================================================================
|
||||
|
||||
public function testToggleEnablesWhenDisabled(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(false);
|
||||
$appConfig->expects($this->once())
|
||||
->method('setValueBool')
|
||||
->with(self::APP_ID, self::DEBUG_KEY, true);
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput([]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $result);
|
||||
$this->assertStringContainsString('enabled', $output->fetch());
|
||||
}
|
||||
|
||||
public function testToggleDisablesWhenEnabled(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(true);
|
||||
$appConfig->expects($this->once())
|
||||
->method('setValueBool')
|
||||
->with(self::APP_ID, self::DEBUG_KEY, false);
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput([]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(0, $result);
|
||||
$this->assertStringContainsString('disabled', $output->fetch());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Conflict: --enable + --disable
|
||||
// =========================================================================
|
||||
|
||||
public function testEnableAndDisableTogetherReturnsInvalid(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(false);
|
||||
// Should NOT be called when both flags are set
|
||||
$appConfig->expects($this->never())
|
||||
->method('setValueBool');
|
||||
|
||||
$command = $this->makeCommand($appConfig);
|
||||
$input = new ArrayInput(['--enable' => true, '--disable' => true]);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$result = $command->run($input, $output);
|
||||
|
||||
$this->assertEquals(1, $result); // Command::INVALID
|
||||
$this->assertStringContainsString('Cannot use both', $output->fetch());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace OCA\Ncbotwebhooks\Test;
|
||||
|
||||
use OCA\Ncbotwebhooks\Cron\ImageCleanup;
|
||||
use OCA\Ncbotwebhooks\Service\TalkService;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\IUserManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Unit tests for ImageCleanup cron job.
|
||||
*
|
||||
* Tests the full run() flow: bot user check, directory check, purge execution.
|
||||
*/
|
||||
class ImageCleanupTest extends TestCase {
|
||||
/**
|
||||
* Build an ImageCleanup with all dependencies mocked.
|
||||
*/
|
||||
private function makeImageCleanup(
|
||||
TalkService $talkService,
|
||||
IRootFolder $rootFolder,
|
||||
IUserManager $userManager,
|
||||
LoggerInterface $logger,
|
||||
): ImageCleanup {
|
||||
return new ImageCleanup(
|
||||
$talkService,
|
||||
$rootFolder,
|
||||
$userManager,
|
||||
$logger,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Early exit paths
|
||||
// =========================================================================
|
||||
|
||||
public function testRunReturnsEarlyWhenNoBotUser(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$userManager = $this->createMock(IUserManager::class);
|
||||
$userManager->method('get')
|
||||
->willReturn(null); // no bot user
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
// Should NOT log anything since we exit early
|
||||
$logger->expects($this->never())
|
||||
->method('info');
|
||||
|
||||
$rootFolder = $this->createMock(IRootFolder::class);
|
||||
|
||||
$cleanup = $this->makeImageCleanup($talkService, $rootFolder, $userManager, $logger);
|
||||
$cleanup->run(); // should not throw
|
||||
}
|
||||
|
||||
public function testRunReturnsEarlyWhenNoImagesDirectory(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
$botUser = $this->createMock(\OCP\IUser::class);
|
||||
$botUser->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManager->method('get')
|
||||
->willReturn($botUser);
|
||||
|
||||
$rootFolder = $this->createMock(IRootFolder::class);
|
||||
$rootFolder->method('getUserFolder')
|
||||
->willReturnCallback(function () {
|
||||
$folder = $this->createMock(\OCP\Files\Folder::class);
|
||||
$folder->expects($this->once())
|
||||
->method('get')
|
||||
->willThrowException(new NotFoundException());
|
||||
return $folder;
|
||||
});
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->never())
|
||||
->method('info');
|
||||
|
||||
$cleanup = $this->makeImageCleanup($talkService, $rootFolder, $userManager, $logger);
|
||||
$cleanup->run(); // should not throw
|
||||
}
|
||||
|
||||
public function testRunReturnsEarlyOnRootFolderException(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
$botUser = $this->createMock(\OCP\IUser::class);
|
||||
$botUser->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManager->method('get')
|
||||
->willReturn($botUser);
|
||||
|
||||
$rootFolder = $this->createMock(IRootFolder::class);
|
||||
$rootFolder->method('getUserFolder')
|
||||
->willThrowException(new \Exception('filesystem error'));
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->never())
|
||||
->method('info');
|
||||
|
||||
$cleanup = $this->makeImageCleanup($talkService, $rootFolder, $userManager, $logger);
|
||||
$cleanup->run(); // should not throw
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Success paths
|
||||
// =========================================================================
|
||||
|
||||
public function testRunPurgesOldImagesAndLogsCount(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('purgeOldImages')
|
||||
->willReturn(5); // purged 5 files
|
||||
|
||||
$userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
$botUser = $this->createMock(\OCP\IUser::class);
|
||||
$botUser->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManager->method('get')
|
||||
->willReturn($botUser);
|
||||
|
||||
$rootFolder = $this->createMock(IRootFolder::class);
|
||||
$imagesFolder = $this->createMock(\OCP\Files\Folder::class);
|
||||
$userFolder = $this->createMock(\OCP\Files\Folder::class);
|
||||
|
||||
$userFolder->method('get')
|
||||
->willReturn($imagesFolder);
|
||||
|
||||
$rootFolder->method('getUserFolder')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->once())
|
||||
->method('info')
|
||||
->with(
|
||||
'nc_bot_webhooks: purged 5 old image files',
|
||||
['app' => 'nc_bot_webhooks'],
|
||||
);
|
||||
|
||||
$cleanup = $this->makeImageCleanup($talkService, $rootFolder, $userManager, $logger);
|
||||
$cleanup->run();
|
||||
}
|
||||
|
||||
public function testRunDoesNotLogWhenNoFilesPurged(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('purgeOldImages')
|
||||
->willReturn(0); // no files purged
|
||||
|
||||
$userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
$botUser = $this->createMock(\OCP\IUser::class);
|
||||
$botUser->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManager->method('get')
|
||||
->willReturn($botUser);
|
||||
|
||||
$rootFolder = $this->createMock(IRootFolder::class);
|
||||
$imagesFolder = $this->createMock(\OCP\Files\Folder::class);
|
||||
$userFolder = $this->createMock(\OCP\Files\Folder::class);
|
||||
|
||||
$userFolder->method('get')
|
||||
->willReturn($imagesFolder);
|
||||
|
||||
$rootFolder->method('getUserFolder')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
// Should NOT log when count is 0
|
||||
$logger->expects($this->never())
|
||||
->method('info');
|
||||
|
||||
$cleanup = $this->makeImageCleanup($talkService, $rootFolder, $userManager, $logger);
|
||||
$cleanup->run();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Edge cases
|
||||
// =========================================================================
|
||||
|
||||
public function testRunWithNullArgument(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('purgeOldImages')
|
||||
->willReturn(3);
|
||||
|
||||
$userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
$botUser = $this->createMock(\OCP\IUser::class);
|
||||
$botUser->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManager->method('get')
|
||||
->willReturn($botUser);
|
||||
|
||||
$rootFolder = $this->createMock(IRootFolder::class);
|
||||
$imagesFolder = $this->createMock(\OCP\Files\Folder::class);
|
||||
$userFolder = $this->createMock(\OCP\Files\Folder::class);
|
||||
|
||||
$userFolder->method('get')
|
||||
->willReturn($imagesFolder);
|
||||
|
||||
$rootFolder->method('getUserFolder')
|
||||
->willReturn($userFolder);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->once())
|
||||
->method('info');
|
||||
|
||||
$cleanup = $this->makeImageCleanup($talkService, $rootFolder, $userManager, $logger);
|
||||
$cleanup->run(null); // null argument should work the same as no argument
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace OCA\Ncbotwebhooks\Test;
|
||||
|
||||
use OCA\Ncbotwebhooks\NavigationProvider;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for NavigationProvider settings link.
|
||||
*
|
||||
* Tests that the navigation link is only shown to admin users.
|
||||
*/
|
||||
class NavigationProviderTest extends TestCase {
|
||||
/**
|
||||
* Build a NavigationProvider with all dependencies mocked.
|
||||
*/
|
||||
private function makeNavigationProvider(
|
||||
IURLGenerator $urlGenerator,
|
||||
IUserSession $userSession,
|
||||
IConfig $config,
|
||||
IL10N $l10n,
|
||||
): NavigationProvider {
|
||||
return new NavigationProvider(
|
||||
$urlGenerator,
|
||||
$userSession,
|
||||
$config,
|
||||
$l10n,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Non-admin paths
|
||||
// =========================================================================
|
||||
|
||||
public function testGetNavigationReturnsEmptyForNullUser(): void {
|
||||
$urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
$userSession->method('getUser')
|
||||
->willReturn(null);
|
||||
$config = $this->createMock(IConfig::class);
|
||||
$l10n = $this->createMock(IL10N::class);
|
||||
|
||||
$provider = $this->makeNavigationProvider($urlGenerator, $userSession, $config, $l10n);
|
||||
$nav = $provider->getNavigation();
|
||||
|
||||
$this->assertEquals([], $nav);
|
||||
}
|
||||
|
||||
public function testGetNavigationReturnsEmptyForNonAdminUser(): void {
|
||||
$urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('isAdmin')
|
||||
->willReturn(false);
|
||||
$userSession->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$config = $this->createMock(IConfig::class);
|
||||
$l10n = $this->createMock(IL10N::class);
|
||||
|
||||
$provider = $this->makeNavigationProvider($urlGenerator, $userSession, $config, $l10n);
|
||||
$nav = $provider->getNavigation();
|
||||
|
||||
$this->assertEquals([], $nav);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Admin path
|
||||
// =========================================================================
|
||||
|
||||
public function testGetNavigationReturnsLinkForAdminUser(): void {
|
||||
$urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$urlGenerator->method('linkToRoute')
|
||||
->willReturn('https://example.com/settings/admin');
|
||||
$urlGenerator->method('imagePath')
|
||||
->willReturn('https://example.com/apps/nc_bot_webhooks/app.svg');
|
||||
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('isAdmin')
|
||||
->willReturn(true);
|
||||
$userSession->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$config = $this->createMock(IConfig::class);
|
||||
|
||||
$l10n = $this->createMock(IL10N::class);
|
||||
$l10n->method('t')
|
||||
->willReturnCallback(function ($text) {
|
||||
return $text;
|
||||
});
|
||||
|
||||
$provider = $this->makeNavigationProvider($urlGenerator, $userSession, $config, $l10n);
|
||||
$nav = $provider->getNavigation();
|
||||
|
||||
$this->assertNotEmpty($nav);
|
||||
$this->assertCount(1, $nav);
|
||||
|
||||
$item = $nav[0];
|
||||
$this->assertEquals('nc_bot_webhooks', $item['id']);
|
||||
$this->assertEquals('nc_bot_webhooks', $item['app_id']);
|
||||
$this->assertEquals('settings', $item['type']);
|
||||
$this->assertEquals('https://example.com/settings/admin', $item['href']);
|
||||
$this->assertEquals('https://example.com/apps/nc_bot_webhooks/app.svg', $item['icon']);
|
||||
$this->assertEquals(0, $item['order']);
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
# nc_bot_webhooks Test Suite
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
```bash
|
||||
cd nc_bot_webhooks
|
||||
composer install
|
||||
composer test
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.2+
|
||||
- Composer
|
||||
|
||||
## What Each Test File Covers
|
||||
|
||||
### `TalkServiceTest.php`
|
||||
|
||||
Unit tests for `Service/TalkService.php` business logic. All TalkService constructor dependencies are mocked via `PHPUnit\Framework\TestCase::getMockForTrait()`.
|
||||
|
||||
**Test groups:**
|
||||
|
||||
- **validateBotPassword** — empty string, valid password, special chars, unicode
|
||||
- **getBaseUrl** — overwritehost priority, overwritewebroot, trusted_domains filtering, CLI URL fallback, empty config
|
||||
- **getRooms / setRooms** — JSON persistence round-trip, empty config, invalid JSON
|
||||
- **getAuthTokens / setAuthTokens** — JSON persistence, empty config
|
||||
- **validateAuthToken** — valid token, invalid token, nonexistent room, multiple tokens
|
||||
- **generateAuthToken** — 48-char hex token, uniqueness across calls
|
||||
- **revokeAuthToken** — removes specific token, cleans empty room arrays, nonexistent token
|
||||
- **mapPayload** — content only, embeds with title/description/fields, empty payload, multiple embeds
|
||||
- **mapApprisePayload** — body only, title fallback, type icons, image type with attachments, nested data key
|
||||
- **getSenderName** — sender_name > username > config default priority
|
||||
- **getSenderNameDefault** — config default
|
||||
- **prependDisplayName** — with/without message, with type icon
|
||||
- **downloadImage** — successful download, empty body
|
||||
- **purgeOldImages** — files older/newer than cutoff, empty directory, bot user not found
|
||||
|
||||
### `WebhookControllerTest.php`
|
||||
|
||||
Unit tests for `Controller/WebhookController.php` HTTP endpoints. Tests response codes and headers for the controller methods that don't depend on `php://input` (auth validation and room listing), plus fallback paths that use `$_GET` and `$_POST` directly.
|
||||
|
||||
**Test groups:**
|
||||
|
||||
- **receive** — invalid auth token (401), empty body (400), $_GET fallback with content, $_GET fallback with embeds
|
||||
- **receiveApprise** — invalid auth token (401), empty body (400), $_POST fallback with notifications, $_POST with image type, $_GET fallback with notifications
|
||||
- **receiveAppriseNotify** — delegates to receiveApprise (401)
|
||||
- **saveConfig** — error handling, invalid config (400), success with auth_tokens (200)
|
||||
- **saveBotPassword** — invalid password (400), valid password (200)
|
||||
- **getRooms** — empty rooms, room list with configured flag, exception → 500
|
||||
- **debug** — disabled (403), enabled for admin (200), enabled for non-admin (200)
|
||||
|
||||
### `ImageCleanupTest.php`
|
||||
|
||||
Unit tests for `Cron/ImageCleanup.php` cron job. Tests the full run() flow including early-exit paths and success logging.
|
||||
|
||||
**Test groups:**
|
||||
|
||||
- **Early exits** — no bot user, no images directory, root folder exception
|
||||
- **Success** — purge runs and logs count, no log when count is 0
|
||||
- **Edge cases** — null argument handling
|
||||
|
||||
### `NavigationProviderTest.php`
|
||||
|
||||
Unit tests for `NavigationProvider` settings link. Tests that the navigation link is only shown to admin users.
|
||||
|
||||
**Test groups:**
|
||||
|
||||
- **Non-admin** — null user, non-admin user → empty array
|
||||
- **Admin** — admin user → navigation link with correct metadata
|
||||
|
||||
### `DebugToggleTest.php`
|
||||
|
||||
Unit tests for `Command/DebugToggle.php` OCC command. Tests --status, --enable, --disable, toggle, and --enable/--disable conflict.
|
||||
|
||||
**Test groups:**
|
||||
|
||||
- **--status** — disabled by default, enabled when set
|
||||
- **--enable** — sets value to true, logs warning
|
||||
- **--disable** — sets value to false, logs success
|
||||
- **Toggle** — enables when disabled, disables when enabled
|
||||
- **Conflict** — --enable + --disable together returns INVALID (1)
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
1. Determine which service or controller the new code belongs to.
|
||||
2. Add test methods to the appropriate test file.
|
||||
3. Use `makeTalkServiceMock()` (WebhookControllerTest) or the helper methods in TalkServiceTest to set up mocks.
|
||||
4. Run `composer test` to verify.
|
||||
|
||||
## Test Architecture
|
||||
|
||||
- **TalkService dependencies**: All 17 constructor parameters are mocked using PHPUnit's `getMockForTrait()` for abstract/interface types and `createMock()` for concrete classes.
|
||||
- **WebhookController dependencies**: TalkService is mocked; the other 9 dependencies use `createMock()`.
|
||||
- **ImageCleanup dependencies**: TalkService, IRootFolder, IUserManager, LoggerInterface — all mocked.
|
||||
- **NavigationProvider dependencies**: IURLGenerator, IUserSession, IConfig, IL10N — all mocked.
|
||||
- **DebugToggle dependencies**: IAppConfig — mocked; Symfony Console Input/Output used for CLI interaction.
|
||||
- **No real Nextcloud instance**: Tests verify business logic in isolation. Image upload/download tests verify the download logic but not real file storage.
|
||||
- **php://input**: The controller reads from `php://input` which cannot be mocked directly. Tests cover:
|
||||
- Auth-failure path (no input needed)
|
||||
- `$_GET` fallback for receive (simulated via `$_GET` in test setup)
|
||||
- `$_POST` fallback for receiveApprise (simulated via `$_POST` in test setup)
|
||||
- Payload parsing / mapping logic is covered by TalkServiceTest.
|
||||
|
||||
## CI
|
||||
|
||||
Tests run automatically on every push and PR via Gitea Actions. See `.gitea/workflows/ci.yml`.
|
||||
|
||||
## Test Summary
|
||||
|
||||
| File | Tests | Coverage |
|
||||
|------|-------|----------|
|
||||
| TalkServiceTest.php | 48 | All TalkService methods |
|
||||
| WebhookControllerTest.php | 21 | All controller endpoints + fallbacks |
|
||||
| ImageCleanupTest.php | 6 | All run() paths |
|
||||
| NavigationProviderTest.php | 3 | Admin/non-admin navigation |
|
||||
| DebugToggleTest.php | 7 | --status, --enable, --disable, toggle, conflict |
|
||||
| **Total** | **85** | All lib/ components covered |
|
||||
@@ -0,0 +1,987 @@
|
||||
<?php
|
||||
|
||||
namespace OCA\Ncbotwebhooks\Tests;
|
||||
|
||||
use OCA\Ncbotwebhooks\Service\TalkService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for TalkService business logic.
|
||||
*
|
||||
* All TalkService constructor dependencies are mocked — no real Nextcloud
|
||||
* instance is required. TalkServiceMockBuilder (in bootstrap.php) assembles
|
||||
* the mocks; each test configures the specific expectations it needs.
|
||||
*/
|
||||
class TalkServiceTest extends TestCase {
|
||||
|
||||
/**
|
||||
* Build a TalkService with a crypto mock that round-trips any string.
|
||||
*
|
||||
* @return array{0: TalkService, 1: \PHPUnit\Framework\MockObject\MockObject}
|
||||
*/
|
||||
private function makeService(): array {
|
||||
$builder = new TalkServiceMockBuilder();
|
||||
$crypto = $builder->config; // the config mock — we swap it below
|
||||
|
||||
// We need direct access to the crypto mock to configure encrypt/decrypt
|
||||
// TalkServiceMockBuilder stores them as properties; we re-construct.
|
||||
$cryptoMock = $this->createMock(\OCP\Security\ICrypto::class);
|
||||
$cryptoMock->method('encrypt')
|
||||
->willReturnCallback(fn($s) => 'encrypted:' . $s);
|
||||
$cryptoMock->method('decrypt')
|
||||
->willReturnCallback(fn($s) => substr($s, 10)); // strip 'encrypted:'
|
||||
|
||||
// Build with our crypto mock
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$this->createMock(\OCP\IConfig::class),
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$cryptoMock,
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
return [$service, $cryptoMock];
|
||||
}
|
||||
|
||||
// ── validateBotPassword ──────────────────────────────────────
|
||||
|
||||
public function testValidateBotPasswordValid(): void {
|
||||
$service = $this->makeService()[0];
|
||||
$result = $service->validateBotPassword('mySecurePassword123!');
|
||||
$this->assertTrue($result['valid']);
|
||||
}
|
||||
|
||||
public function testValidateBotPasswordEmpty(): void {
|
||||
$service = $this->makeService()[0];
|
||||
$result = $service->validateBotPassword('');
|
||||
$this->assertFalse($result['valid']);
|
||||
$this->assertStringContainsString('empty', $result['error']);
|
||||
}
|
||||
|
||||
public function testValidateBotPasswordSpecialChars(): void {
|
||||
$service = $this->makeService()[0];
|
||||
$result = $service->validateBotPassword("!@#\$%^&*()_+-=[]{}|;:',.<>?/~`");
|
||||
$this->assertTrue($result['valid']);
|
||||
}
|
||||
|
||||
public function testValidateBotPasswordUnicode(): void {
|
||||
$service = $this->makeService()[0];
|
||||
$result = $service->validateBotPassword('密码🔒🎉');
|
||||
$this->assertTrue($result['valid']);
|
||||
}
|
||||
|
||||
// ── getBaseUrl ───────────────────────────────────────────────
|
||||
|
||||
public function testGetBaseUrlOverwriteHostTakesPriority(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', 'example.com'],
|
||||
['overwriteproto', 'https', 'https'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame('https://example.com', $service->getBaseUrl());
|
||||
}
|
||||
|
||||
public function testGetBaseUrlOverwritewebrootFullPath(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', ''],
|
||||
['overwritewebroot', '', 'https://webroot.example.com/nextcloud'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame('https://webroot.example.com/nextcloud', $service->getBaseUrl());
|
||||
}
|
||||
|
||||
public function testGetBaseUrlOverwritewebrootPath(): void {
|
||||
// Path-style overwritewebroot should fall through to trusted_domains
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', ''],
|
||||
['overwritewebroot', '', '/nextcloud'],
|
||||
]);
|
||||
$trustedMock = $this->createMock(\OCP\IConfig::class);
|
||||
$trustedMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', ''],
|
||||
['overwritewebroot', '', '/nextcloud'],
|
||||
]);
|
||||
$trustedMock->method('getSystemValue')
|
||||
->willReturn(['public.example.com']);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$trustedMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame('https://public.example.com', $service->getBaseUrl());
|
||||
}
|
||||
|
||||
public function testGetBaseUrlSkipsPrivateDomains(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', ''],
|
||||
['overwritewebroot', '', ''],
|
||||
]);
|
||||
$configMock->method('getSystemValue')
|
||||
->willReturn(['192.168.1.100', '10.0.0.5', 'public.example.com']);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame('https://public.example.com', $service->getBaseUrl());
|
||||
}
|
||||
|
||||
public function testGetBaseUrlFallbackToCliUrl(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', ''],
|
||||
['overwritewebroot', '', ''],
|
||||
]);
|
||||
$configMock->method('getSystemValue')
|
||||
->willReturn([]);
|
||||
$configMock->method('getSystemValueString')
|
||||
->willReturnMap([
|
||||
['overwritehost', '', ''],
|
||||
['overwritewebroot', '', ''],
|
||||
['overwrite.cli.url', '', 'https://cli.example.com'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame('https://cli.example.com', $service->getBaseUrl());
|
||||
}
|
||||
|
||||
public function testGetBaseUrlEmptyConfig(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getSystemValueString')->willReturn('');
|
||||
$configMock->method('getSystemValue')->willReturn([]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame('', $service->getBaseUrl());
|
||||
}
|
||||
|
||||
// ── getRooms / setRooms ──────────────────────────────────────
|
||||
|
||||
public function testGetRoomsEmpty(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnMap([
|
||||
[TalkService::APP_ID, 'rooms', '[]', '[]'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame([], $service->getRooms());
|
||||
}
|
||||
|
||||
public function testGetRoomsParsed(): void {
|
||||
$json = json_encode(['abc123' => 'General Chat', 'xyz789' => 'Builds']);
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnMap([
|
||||
[TalkService::APP_ID, 'rooms', '[]', $json],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$rooms = $service->getRooms();
|
||||
$this->assertArrayHasKey('abc123', $rooms);
|
||||
$this->assertSame('General Chat', $rooms['abc123']);
|
||||
$this->assertArrayHasKey('xyz789', $rooms);
|
||||
$this->assertSame('Builds', $rooms['xyz789']);
|
||||
}
|
||||
|
||||
public function testSetRoomsPersists(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnMap([
|
||||
[TalkService::APP_ID, 'rooms', '[]', '[]'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
$rooms = ['room1' => 'Room One'];
|
||||
$configMock->expects($this->once())
|
||||
->method('setAppValue')
|
||||
->with(TalkService::APP_ID, 'rooms', json_encode($rooms));
|
||||
|
||||
$service->setRooms($rooms);
|
||||
}
|
||||
|
||||
// ── getAuthTokens / setAuthTokens ────────────────────────────
|
||||
|
||||
public function testGetAuthTokensEmpty(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnMap([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', '{}'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$this->assertSame([], $service->getAuthTokens());
|
||||
}
|
||||
|
||||
public function testGetAuthTokensParsed(): void {
|
||||
$json = json_encode(['abc123' => ['tok1', 'tok2'], 'xyz789' => ['tok3']]);
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnMap([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', $json],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
$tokens = $service->getAuthTokens();
|
||||
$this->assertSame(['tok1', 'tok2'], $tokens['abc123']);
|
||||
$this->assertSame(['tok3'], $tokens['xyz789']);
|
||||
}
|
||||
|
||||
public function testSetAuthTokensPersists(): void {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnMap([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', '{}'],
|
||||
]);
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
$tokens = ['abc123' => ['tok1']];
|
||||
$configMock->expects($this->once())
|
||||
->method('setAppValue')
|
||||
->with(TalkService::APP_ID, 'auth_tokens', json_encode($tokens));
|
||||
|
||||
$service->setAuthTokens($tokens);
|
||||
}
|
||||
|
||||
// ── validateAuthToken ────────────────────────────────────────
|
||||
|
||||
public function testValidateAuthTokenValid(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1', 'tok2']])],
|
||||
]);
|
||||
$this->assertTrue($service->validateAuthToken('abc123', 'tok1'));
|
||||
$this->assertTrue($service->validateAuthToken('abc123', 'tok2'));
|
||||
}
|
||||
|
||||
public function testValidateAuthTokenInvalid(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1', 'tok2']])],
|
||||
]);
|
||||
$this->assertFalse($service->validateAuthToken('abc123', 'tok3'));
|
||||
}
|
||||
|
||||
public function testValidateAuthTokenNonexistentRoom(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1']])],
|
||||
]);
|
||||
$this->assertFalse($service->validateAuthToken('xyz789', 'tok1'));
|
||||
}
|
||||
|
||||
public function testValidateAuthTokenMultipleTokens(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1', 'tok2', 'tok3']])],
|
||||
]);
|
||||
$this->assertTrue($service->validateAuthToken('abc123', 'tok2'));
|
||||
}
|
||||
|
||||
public function testValidateAuthTokenTokenNotInRoom(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1'], 'xyz789' => ['tok2']])],
|
||||
]);
|
||||
$this->assertFalse($service->validateAuthToken('abc123', 'tok2'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: build a TalkService with a configured IConfig mock.
|
||||
*
|
||||
* @param array $valueMap value map for IConfig::getAppValue
|
||||
* @return array{0: TalkService, 1: \PHPUnit\Framework\MockObject\MockObject}
|
||||
*/
|
||||
private function makeServiceWithConfig(array $valueMap): array {
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')
|
||||
->willReturnCallback(function ($app, $key, $default) use ($valueMap, $configMock) {
|
||||
foreach ($valueMap as $entry) {
|
||||
if ($entry[0] === $app && $entry[1] === $key) {
|
||||
return $entry[3] ?? $default;
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
});
|
||||
// setAppValue is a no-op in tests
|
||||
$configMock->method('setAppValue')
|
||||
->willReturnCallback(fn($app, $key, $value) => null);
|
||||
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
return [$service, $configMock];
|
||||
}
|
||||
|
||||
// ── generateAuthToken ────────────────────────────────────────
|
||||
|
||||
public function testGenerateAuthTokenGeneratesToken(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', '{}'],
|
||||
]);
|
||||
$token = $service->generateAuthToken('abc123');
|
||||
$this->assertEquals(48, strlen($token));
|
||||
$this->assertRegExp('/^[0-9a-f]+$/', $token);
|
||||
}
|
||||
|
||||
public function testGenerateAuthTokenPersists(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', '{}'],
|
||||
]);
|
||||
$configMock->expects($this->atLeast(2))
|
||||
->method('setAppValue')
|
||||
->willReturnCallback(fn($app, $key, $value) => null);
|
||||
|
||||
$token1 = $service->generateAuthToken('abc123');
|
||||
$token2 = $service->generateAuthToken('abc123');
|
||||
|
||||
$this->assertNotSame($token1, $token2);
|
||||
}
|
||||
|
||||
// ── revokeAuthToken ──────────────────────────────────────────
|
||||
|
||||
public function testRevokeAuthTokenRemovesToken(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1', 'tok2']])],
|
||||
]);
|
||||
$configMock->expects($this->atLeast(2))
|
||||
->method('setAppValue')
|
||||
->willReturnCallback(fn($app, $key, $value) => null);
|
||||
|
||||
$service->revokeAuthToken('abc123', 'tok1');
|
||||
|
||||
$this->assertFalse($service->validateAuthToken('abc123', 'tok1'));
|
||||
$this->assertTrue($service->validateAuthToken('abc123', 'tok2'));
|
||||
}
|
||||
|
||||
public function testRevokeAuthTokenCleansEmptyRoom(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1']])],
|
||||
]);
|
||||
$configMock->expects($this->atLeast(2))
|
||||
->method('setAppValue')
|
||||
->willReturnCallback(fn($app, $key, $value) => null);
|
||||
|
||||
$service->revokeAuthToken('abc123', 'tok1');
|
||||
|
||||
$tokens = $service->getAuthTokens();
|
||||
$this->assertArrayNotHasKey('abc123', $tokens);
|
||||
}
|
||||
|
||||
public function testRevokeAuthTokenNonexistentToken(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'auth_tokens', '{}', json_encode(['abc123' => ['tok1']])],
|
||||
]);
|
||||
$configMock->expects($this->once())
|
||||
->method('setAppValue')
|
||||
->willReturnCallback(fn($app, $key, $value) => null);
|
||||
|
||||
$service->revokeAuthToken('abc123', 'nonexistent');
|
||||
|
||||
// Should still have tok1
|
||||
$this->assertTrue($service->validateAuthToken('abc123', 'tok1'));
|
||||
}
|
||||
|
||||
// ── mapPayload ───────────────────────────────────────────────
|
||||
|
||||
public function testMapPayloadContentOnly(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->mapPayload(['content' => 'Hello world']);
|
||||
$this->assertSame('Hello world', $result);
|
||||
}
|
||||
|
||||
public function testMapPayloadEmbedWithTitle(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->mapPayload([
|
||||
'embeds' => [['title' => 'Build #123']],
|
||||
]);
|
||||
$this->assertStringContainsString('**Build #123**', $result);
|
||||
}
|
||||
|
||||
public function testMapPayloadEmbedWithDescription(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->mapPayload([
|
||||
'embeds' => [['description' => 'Passed']],
|
||||
]);
|
||||
$this->assertStringContainsString('Passed', $result);
|
||||
}
|
||||
|
||||
public function testMapPayloadEmbedWithFields(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->mapPayload([
|
||||
'embeds' => [[
|
||||
'fields' => [
|
||||
['name' => 'Duration', 'value' => '2m 34s'],
|
||||
['name' => 'Environment', 'value' => 'Production'],
|
||||
],
|
||||
]],
|
||||
]);
|
||||
$this->assertStringContainsString('Duration: 2m 34s', $result);
|
||||
$this->assertStringContainsString('Environment: Production', $result);
|
||||
}
|
||||
|
||||
public function testMapPayloadEmpty(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->mapPayload([]);
|
||||
$this->assertSame('', $result);
|
||||
}
|
||||
|
||||
public function testMapPayloadMultipleEmbeds(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->mapPayload([
|
||||
'embeds' => [
|
||||
['title' => 'First'],
|
||||
['title' => 'Second'],
|
||||
],
|
||||
]);
|
||||
$this->assertStringContainsString('**First**', $result);
|
||||
$this->assertStringContainsString('**Second**', $result);
|
||||
}
|
||||
|
||||
// ── mapApprisePayload ────────────────────────────────────────
|
||||
|
||||
public function testMapApprisePayloadBodyOnly(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'sender_name', 'Webhook Bot', 'Custom Bot'],
|
||||
]);
|
||||
$result = $service->mapApprisePayload(['body' => 'All clear'], 'abc123');
|
||||
$this->assertSame('All clear', $result['message']);
|
||||
}
|
||||
|
||||
public function testMapApprisePayloadTitleFallback(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'sender_name', 'Webhook Bot', 'Default Sender'],
|
||||
]);
|
||||
$result = $service->mapApprisePayload(['title' => 'Alert', 'body' => 'Something happened'], 'abc123');
|
||||
$this->assertSame('Something happened', $result['message']);
|
||||
$this->assertSame('Alert', $result['senderName']);
|
||||
}
|
||||
|
||||
public function testMapApprisePayloadTypeIcons(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([]);
|
||||
// Success type — should include ✅ icon
|
||||
$resultSuccess = $service->mapApprisePayload(['body' => 'OK', 'type' => 'success'], 'abc123');
|
||||
$this->assertStringContainsString('✅', $resultSuccess['message']);
|
||||
|
||||
// Warning type
|
||||
$resultWarning = $service->mapApprisePayload(['body' => 'Warn', 'type' => 'warning'], 'abc123');
|
||||
$this->assertStringContainsString('⚠️', $resultWarning['message']);
|
||||
|
||||
// Error type
|
||||
$resultError = $service->mapApprisePayload(['body' => 'Fail', 'type' => 'error'], 'abc123');
|
||||
$this->assertStringContainsString('❌', $resultError['message']);
|
||||
}
|
||||
|
||||
public function testMapApprisePayloadImageType(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([]);
|
||||
// Image type with attachment URLs — processImageUrls is called, which returns empty
|
||||
// because there's no real file system; verify richObjects is set
|
||||
$result = $service->mapApprisePayload([
|
||||
'body' => 'Check this out',
|
||||
'type' => 'image',
|
||||
'attachments' => [['url' => 'https://example.com/img.png']],
|
||||
], 'abc123');
|
||||
// richObjects should be an array (possibly empty due to no real FS)
|
||||
$this->assertIsArray($result['richObjects']);
|
||||
}
|
||||
|
||||
public function testMapApprisePayloadNestedDataKey(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([]);
|
||||
// When the payload is wrapped in a 'data' key (Home Assistant), the controller
|
||||
// unwraps it before passing to mapApprisePayload. Here we test the unwrapped form.
|
||||
$result = $service->mapApprisePayload([
|
||||
'body' => 'Direct payload',
|
||||
'title' => 'Direct Title',
|
||||
], 'abc123');
|
||||
$this->assertSame('Direct payload', $result['message']);
|
||||
$this->assertSame('Direct Title', $result['senderName']);
|
||||
}
|
||||
|
||||
// ── getSenderName ────────────────────────────────────────────
|
||||
|
||||
public function testGetSenderNameSenderNameTakesPriority(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'sender_name', 'Webhook Bot', 'Default Sender'],
|
||||
]);
|
||||
$this->assertSame('Custom', $service->getSenderName(['sender_name' => 'Custom']));
|
||||
}
|
||||
|
||||
public function testGetSenderNameUsernameFallback(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'sender_name', 'Webhook Bot', 'Default Sender'],
|
||||
]);
|
||||
$this->assertSame('CI Bot', $service->getSenderName(['username' => 'CI Bot']));
|
||||
}
|
||||
|
||||
public function testGetSenderNameConfigDefault(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'sender_name', 'Webhook Bot', 'Default Sender'],
|
||||
]);
|
||||
$this->assertSame('Default Sender', $service->getSenderName([]));
|
||||
}
|
||||
|
||||
// ── getSenderNameDefault ─────────────────────────────────────
|
||||
|
||||
public function testGetSenderNameDefault(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([
|
||||
[TalkService::APP_ID, 'sender_name', 'Webhook Bot', 'My Bot'],
|
||||
]);
|
||||
$this->assertSame('My Bot', $service->getSenderNameDefault());
|
||||
}
|
||||
|
||||
// ── prependDisplayName ───────────────────────────────────────
|
||||
|
||||
public function testPrependDisplayNameWithMessage(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->prependDisplayName('CI Bot', 'Build passed');
|
||||
$this->assertStringContainsString('**CI Bot**', $result);
|
||||
$this->assertStringContainsString('Build passed', $result);
|
||||
$this->assertStringContainsString("\n\n", $result);
|
||||
}
|
||||
|
||||
public function testPrependDisplayNameWithoutMessage(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->prependDisplayName('CI Bot', '');
|
||||
$this->assertStringContainsString('**CI Bot**', $result);
|
||||
$this->assertStringNotContainsString("\n\n", $result);
|
||||
}
|
||||
|
||||
public function testPrependDisplayNameWithTypeIcon(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
$result = $service->prependDisplayName('CI Bot', 'OK', '✅');
|
||||
$this->assertStringContainsString('✅', $result);
|
||||
$this->assertStringContainsString('**CI Bot**', $result);
|
||||
}
|
||||
|
||||
// ── downloadImage ────────────────────────────────────────────
|
||||
|
||||
public function testDownloadImageSuccessful(): void {
|
||||
[$service, $configMock] = $this->makeServiceWithConfig([]);
|
||||
|
||||
$responseMock = $this->createMock(\Psr\Http\Message\ResponseInterface::class);
|
||||
$responseMock->method('getBody')
|
||||
->willReturn(new class {
|
||||
public function __toString(): string { return 'image-data'; }
|
||||
});
|
||||
|
||||
$clientMock = $this->createMock(\OCP\Http\Client\IClient::class);
|
||||
$clientMock->method('get')
|
||||
->willReturn($responseMock);
|
||||
|
||||
$clientServiceMock = $this->createMock(\OCP\Http\Client\IClientService::class);
|
||||
$clientServiceMock->method('getClient')
|
||||
->willReturn($clientMock);
|
||||
|
||||
// Rebuild with our client service
|
||||
$configMock2 = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock2->method('getAppValue')->willReturn('');
|
||||
$service2 = new TalkService(
|
||||
$clientServiceMock,
|
||||
$configMock2,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
$result = $service2->downloadImage('https://example.com/image.png');
|
||||
$this->assertSame('image-data', $result);
|
||||
}
|
||||
|
||||
public function testDownloadImageEmptyBody(): void {
|
||||
[$service] = $this->makeServiceWithConfig([]);
|
||||
|
||||
$responseMock = $this->createMock(\Psr\Http\Message\ResponseInterface::class);
|
||||
$responseMock->method('getBody')
|
||||
->willReturn(new class {
|
||||
public function __toString(): string { return ''; }
|
||||
});
|
||||
|
||||
$clientMock = $this->createMock(\OCP\Http\Client\IClient::class);
|
||||
$clientMock->method('get')
|
||||
->willReturn($responseMock);
|
||||
|
||||
$clientServiceMock = $this->createMock(\OCP\Http\Client\IClientService::class);
|
||||
$clientServiceMock->method('getClient')
|
||||
->willReturn($clientMock);
|
||||
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')->willReturn('');
|
||||
$service2 = new TalkService(
|
||||
$clientServiceMock,
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$this->createMock(\OCP\Files\IRootFolder::class),
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$this->createMock(\OCP\IUserManager::class),
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$this->createMock(\Psr\Log\LoggerInterface::class),
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
$result = $service2->downloadImage('https://example.com/empty.png');
|
||||
$this->assertSame('', $result);
|
||||
}
|
||||
|
||||
// ── purgeOldImages ───────────────────────────────────────────
|
||||
|
||||
public function testPurgeOldImagesWithFiles(): void {
|
||||
// Create a temporary directory to simulate the images folder
|
||||
$tempDir = sys_get_temp_dir() . '/nc_bot_webhooks_test_' . uniqid();
|
||||
mkdir($tempDir . '/abc123', 0755, true);
|
||||
|
||||
// Create a file older than 90 days
|
||||
$oldFile = $tempDir . '/abc123/old.png';
|
||||
file_put_contents($oldFile, 'data');
|
||||
touch($oldFile, time() - (100 * 86400)); // 100 days ago
|
||||
|
||||
// Create a recent file
|
||||
$recentFile = $tempDir . '/abc123/recent.png';
|
||||
file_put_contents($recentFile, 'data');
|
||||
touch($recentFile, time() - (10 * 86400)); // 10 days ago
|
||||
|
||||
// Mock rootFolder to return our temp directory
|
||||
$folderMock = $this->createMock(\OCP\Files\Folder::class);
|
||||
$folderMock->method('getContents')
|
||||
->willReturn(['old.png', 'recent.png']);
|
||||
|
||||
$oldFileMock = $this->createMock(\OCP\Files\File::class);
|
||||
$oldFileMock->method('getMTime')
|
||||
->willReturn(time() - (100 * 86400));
|
||||
|
||||
$recentFileMock = $this->createMock(\OCP\Files\File::class);
|
||||
$recentFileMock->method('getMTime')
|
||||
->willReturn(time() - (10 * 86400));
|
||||
|
||||
$folderMock->method('get')
|
||||
->willReturnMap([
|
||||
['old.png', $oldFileMock],
|
||||
['recent.png', $recentFileMock],
|
||||
]);
|
||||
|
||||
$rootMock = $this->createMock(\OCP\Files\IRootFolder::class);
|
||||
$rootMock->method('getUserFolder')
|
||||
->willReturn($folderMock);
|
||||
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')->willReturn('90');
|
||||
|
||||
$userManagerMock = $this->createMock(\OCP\IUserManager::class);
|
||||
$userMock = $this->createMock(\OCP\IUser::class);
|
||||
$userMock->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManagerMock->method('get')
|
||||
->willReturn($userMock);
|
||||
|
||||
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
|
||||
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$rootMock,
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$userManagerMock,
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$loggerMock,
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
$count = $service->purgeOldImages();
|
||||
$this->assertGreaterThanOrEqual(1, $count);
|
||||
|
||||
// Cleanup
|
||||
unlink($oldFile);
|
||||
unlink($recentFile);
|
||||
rmdir($tempDir . '/abc123');
|
||||
rmdir($tempDir);
|
||||
}
|
||||
|
||||
public function testPurgeOldImagesEmptyDirectory(): void {
|
||||
$folderMock = $this->createMock(\OCP\Files\Folder::class);
|
||||
$folderMock->method('getContents')
|
||||
->willReturn([]);
|
||||
|
||||
$rootMock = $this->createMock(\OCP\Files\IRootFolder::class);
|
||||
$rootMock->method('getUserFolder')
|
||||
->willReturn($folderMock);
|
||||
|
||||
$configMock = $this->createMock(\OCP\IConfig::class);
|
||||
$configMock->method('getAppValue')->willReturn('90');
|
||||
|
||||
$userManagerMock = $this->createMock(\OCP\IUserManager::class);
|
||||
$userMock = $this->createMock(\OCP\IUser::class);
|
||||
$userMock->method('getUID')
|
||||
->willReturn('talk-bot');
|
||||
$userManagerMock->method('get')
|
||||
->willReturn($userMock);
|
||||
|
||||
$loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
|
||||
|
||||
$service = new TalkService(
|
||||
$this->createMock(\OCP\Http\Client\IClientService::class),
|
||||
$configMock,
|
||||
$this->createMock(\OCP\IDBConnection::class),
|
||||
$rootMock,
|
||||
$this->createMock(\OCP\IRequest::class),
|
||||
$this->createMock(\OCP\IURLGenerator::class),
|
||||
$userManagerMock,
|
||||
$this->createMock(\OCP\IUserSession::class),
|
||||
$loggerMock,
|
||||
$this->createMock(\OCA\Talk\Manager::class),
|
||||
$this->createMock(\OCP\Security\ICrypto::class),
|
||||
$this->createMock(\OCA\Talk\Model\AttendeeMapper::class),
|
||||
$this->createMock(\OCP\Share\IManager::class),
|
||||
$this->createMock(\OCA\Talk\Service\ParticipantService::class),
|
||||
$this->createMock(\OCA\Talk\Chat\ChatManager::class),
|
||||
$this->createMock(\OCA\Talk\TalkSession::class),
|
||||
);
|
||||
|
||||
$count = $service->purgeOldImages();
|
||||
$this->assertSame(0, $count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
<?php
|
||||
|
||||
namespace OCA\Ncbotwebhooks\Test;
|
||||
|
||||
use OCA\Ncbotwebhooks\Controller\WebhookController;
|
||||
use OCA\Ncbotwebhooks\Service\TalkService;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IAppManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Share\IManager as IShareManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Unit tests for WebhookController HTTP endpoints.
|
||||
*
|
||||
* The controller reads from php://input internally, which can't be mocked
|
||||
* directly. We test:
|
||||
* - Auth failure path (returns 401, no input needed)
|
||||
* - getRooms (returns structured room list)
|
||||
* - getRooms error handling (returns 500)
|
||||
* - $_GET fallback for receive (when php://input is empty)
|
||||
* - $_POST fallback for receiveApprise
|
||||
* - debug endpoint (disabled/enabled)
|
||||
* - saveConfig/saveBotPassword edge cases
|
||||
*
|
||||
* The payload parsing / mapping logic is covered by TalkServiceTest.
|
||||
*/
|
||||
class WebhookControllerTest extends TestCase {
|
||||
/**
|
||||
* Build a WebhookController with all dependencies mocked.
|
||||
*/
|
||||
private function makeController(TalkService $talkService): WebhookController {
|
||||
return new WebhookController(
|
||||
'nc_bot_webhooks',
|
||||
$this->createMock(IRequest::class),
|
||||
$talkService,
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(IAppManager::class),
|
||||
$this->createMock(IUserSession::class),
|
||||
$this->createMock(IGroupManager::class),
|
||||
$this->createMock(IConfig::class),
|
||||
$this->createMock(IAppConfig::class),
|
||||
$this->createMock(IClientService::class),
|
||||
$this->createMock(IShareManager::class),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a TalkService mock with given method return values.
|
||||
*/
|
||||
private function makeTalkServiceMock(array $methods): TalkService {
|
||||
$mock = $this->createMock(TalkService::class);
|
||||
foreach ($methods as $method => $return) {
|
||||
if (is_callable($return)) {
|
||||
$mock->method($method)
|
||||
->willReturnCallback($return);
|
||||
} else {
|
||||
$mock->method($method)
|
||||
->willReturn($return);
|
||||
}
|
||||
}
|
||||
return $mock;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// receive (Discord webhook)
|
||||
// =========================================================================
|
||||
|
||||
public function testReceiveReturns401ForInvalidAuthToken(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateAuthToken' => false,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->receive('room123', 'badtoken');
|
||||
|
||||
$this->assertEquals(401, $response->getStatus());
|
||||
$this->assertEquals('Unauthorized', $response->getData()['error']);
|
||||
}
|
||||
|
||||
public function testReceiveReturns400ForEmptyBody(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateAuthToken' => true,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
// With no php://input available, json_decode('') fails → 400
|
||||
$response = $controller->receive('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(400, $response->getStatus());
|
||||
$this->assertEquals('Invalid JSON', $response->getData()['error']);
|
||||
}
|
||||
|
||||
public function testReceiveUsesGETFallbackWhenInputIsEmpty(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateAuthToken' => true,
|
||||
'mapPayload' => function ($payload) {
|
||||
return [
|
||||
'message' => $payload['content'] ?? '',
|
||||
'senderName' => 'Bot',
|
||||
'displayName' => 'Bot',
|
||||
'richObjects' => [],
|
||||
];
|
||||
},
|
||||
'getSenderNameDefault' => 'Bot',
|
||||
'prependDisplayName' => function ($name, $msg) {
|
||||
return $msg;
|
||||
},
|
||||
'postToRoom' => true,
|
||||
]);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
// Simulate empty php://input with $_GET fallback
|
||||
$_GET = [
|
||||
'content' => 'GET fallback message',
|
||||
];
|
||||
|
||||
$response = $controller->receive('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(201, $response->getStatus());
|
||||
$this->assertEquals('ok', $response->getData()['status']);
|
||||
|
||||
// Clean up
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
public function testReceiveGETFallbackWithEmbeds(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('validateAuthToken')
|
||||
->willReturn(true);
|
||||
$talkService->method('mapPayload')
|
||||
->willReturnCallback(function ($payload) {
|
||||
return [
|
||||
'message' => $payload['content'] ?? '',
|
||||
'senderName' => 'Bot',
|
||||
'displayName' => 'Bot',
|
||||
'richObjects' => [],
|
||||
];
|
||||
});
|
||||
$talkService->method('getSenderNameDefault')
|
||||
->willReturn('Bot');
|
||||
$talkService->method('prependDisplayName')
|
||||
->willReturnCallback(function ($name, $msg) {
|
||||
return $msg;
|
||||
});
|
||||
$talkService->method('postToRoom')
|
||||
->willReturn(true);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$_GET = [
|
||||
'content' => 'Test with embeds',
|
||||
'embeds' => '[{"title":"Embed Title","description":"Embed desc"}]',
|
||||
];
|
||||
|
||||
$response = $controller->receive('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(201, $response->getStatus());
|
||||
|
||||
// Clean up
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// receiveApprise
|
||||
// =========================================================================
|
||||
|
||||
public function testReceiveAppriseReturns401ForInvalidAuthToken(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateAuthToken' => false,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->receiveApprise('room123', 'badtoken');
|
||||
|
||||
$this->assertEquals(401, $response->getStatus());
|
||||
$this->assertEquals('Unauthorized', $response->getData()['error']);
|
||||
}
|
||||
|
||||
public function testReceiveAppriseReturns400ForEmptyBody(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateAuthToken' => true,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
// No php://input → no data → 400
|
||||
$response = $controller->receiveApprise('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(400, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testReceiveAppriseNotifyDelegatesToReceiveApprise(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateAuthToken' => false,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->receiveAppriseNotify('room123', 'badtoken');
|
||||
|
||||
$this->assertEquals(401, $response->getStatus());
|
||||
$this->assertEquals('Unauthorized', $response->getData()['error']);
|
||||
}
|
||||
|
||||
public function testReceiveAppriseUsesPOSTFallbackWithNotifications(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('validateAuthToken')
|
||||
->willReturn(true);
|
||||
$talkService->method('mapApprisePayload')
|
||||
->willReturn([
|
||||
'message' => 'POST fallback message',
|
||||
'senderName' => 'Apprise',
|
||||
'displayName' => 'Apprise',
|
||||
'richObjects' => [],
|
||||
]);
|
||||
$talkService->method('getSenderNameDefault')
|
||||
->willReturn('Bot');
|
||||
$talkService->method('prependDisplayName')
|
||||
->willReturnCallback(function ($name, $msg) {
|
||||
return $msg;
|
||||
});
|
||||
$talkService->method('postToRoom')
|
||||
->willReturn(true);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
// Simulate $_POST fallback (form-encoded with notifications wrapper)
|
||||
$_POST = [
|
||||
'notifications' => [
|
||||
[
|
||||
'body' => 'POST fallback message',
|
||||
'type' => 'success',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $controller->receiveApprise('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(201, $response->getStatus());
|
||||
|
||||
// Clean up
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
public function testReceiveApprisePOSTFallbackWithImageType(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('validateAuthToken')
|
||||
->willReturn(true);
|
||||
$talkService->method('mapApprisePayload')
|
||||
->willReturn([
|
||||
'message' => '',
|
||||
'senderName' => 'Apprise',
|
||||
'displayName' => 'Apprise',
|
||||
'richObjects' => ['richObject' => []],
|
||||
]);
|
||||
$talkService->method('getSenderNameDefault')
|
||||
->willReturn('Bot');
|
||||
$talkService->method('prependDisplayName')
|
||||
->willReturnCallback(function ($name, $msg) {
|
||||
return $msg;
|
||||
});
|
||||
$talkService->method('postToRoom')
|
||||
->willReturn(true);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$_POST = [
|
||||
'notifications' => [
|
||||
[
|
||||
'body' => 'Image notification',
|
||||
'type' => 'image',
|
||||
'attachments' => ['https://example.com/image.png'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $controller->receiveApprise('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(201, $response->getStatus());
|
||||
|
||||
// Clean up
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
public function testReceiveAppriseGETFallbackWithNotifications(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('validateAuthToken')
|
||||
->willReturn(true);
|
||||
$talkService->method('mapApprisePayload')
|
||||
->willReturn([
|
||||
'message' => 'GET fallback message',
|
||||
'senderName' => 'Apprise',
|
||||
'displayName' => 'Apprise',
|
||||
'richObjects' => [],
|
||||
]);
|
||||
$talkService->method('getSenderNameDefault')
|
||||
->willReturn('Bot');
|
||||
$talkService->method('prependDisplayName')
|
||||
->willReturnCallback(function ($name, $msg) {
|
||||
return $msg;
|
||||
});
|
||||
$talkService->method('postToRoom')
|
||||
->willReturn(true);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
// $_GET with notifications array
|
||||
$_GET = [
|
||||
'notifications' => [
|
||||
[
|
||||
'body' => 'GET fallback message',
|
||||
'type' => 'notice',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$response = $controller->receiveApprise('room123', 'validtoken');
|
||||
|
||||
$this->assertEquals(201, $response->getStatus());
|
||||
|
||||
// Clean up
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// saveConfig / saveBotPassword
|
||||
// =========================================================================
|
||||
|
||||
public function testSaveConfigReturns500WhenTalkServiceFails(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('validateBotPassword')
|
||||
->willThrowException(new \Exception('crypto error'));
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->saveConfig([
|
||||
'bot_password' => 'test',
|
||||
'retention_days' => '90',
|
||||
'sender_name' => 'Bot',
|
||||
'rooms' => [],
|
||||
'auth_tokens' => [],
|
||||
]);
|
||||
|
||||
$this->assertEquals(500, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testSaveConfigReturns400WhenInvalidConfig(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->saveConfig(null);
|
||||
|
||||
$this->assertEquals(400, $response->getStatus());
|
||||
$this->assertEquals('Invalid config', $response->getData()['error']);
|
||||
}
|
||||
|
||||
public function testSaveConfigReturns200WithAuthTokens(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('validateBotPassword')
|
||||
->willReturn(true);
|
||||
$talkService->method('saveConfig')
|
||||
->willReturn(null);
|
||||
$talkService->method('getAuthTokens')
|
||||
->willReturn(['room1' => 'token1']);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->saveConfig([
|
||||
'bot_password' => 'validpassword',
|
||||
'retention_days' => '30',
|
||||
'sender_name' => 'TestBot',
|
||||
'rooms' => ['room1'],
|
||||
'auth_tokens' => ['room1' => 'token1'],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals('ok', $response->getData()['status']);
|
||||
$this->assertEquals(['room1' => 'token1'], $response->getData()['auth_tokens']);
|
||||
}
|
||||
|
||||
public function testSaveBotPasswordRejectsInvalidPassword(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateBotPassword' => false,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->saveBotPassword('invalid');
|
||||
|
||||
$this->assertEquals(400, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testSaveBotPasswordAcceptsValidPassword(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'validateBotPassword' => true,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->saveBotPassword('validpassword123');
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getRooms
|
||||
// =========================================================================
|
||||
|
||||
public function testGetRoomsReturnsEmptyArrayWhenNoRooms(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'getAvailableTalkRooms' => [],
|
||||
'getRooms' => [],
|
||||
'detectTalkTableFromCatalog' => null,
|
||||
]);
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->getRooms();
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertEquals([], $response->getData());
|
||||
}
|
||||
|
||||
public function testGetRoomsReturnsRoomListWithConfiguredFlag(): void {
|
||||
$talkService = $this->makeTalkServiceMock([
|
||||
'getAvailableTalkRooms' => [
|
||||
'abc123' => 'Test Room',
|
||||
'def456' => 'Another Room',
|
||||
],
|
||||
'getRooms' => ['abc123' => true], // only abc123 configured
|
||||
'detectTalkTableFromCatalog' => 'talk_rooms',
|
||||
]);
|
||||
|
||||
$dbConn = $this->createMock(\Doctrine\DBAL\Connection::class);
|
||||
$dbConn->method('executeQuery')
|
||||
->willReturn(null);
|
||||
$talkService->method('getDbConnection')
|
||||
->willReturn($dbConn);
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->getRooms();
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$data = $response->getData();
|
||||
$this->assertNotEmpty($data);
|
||||
|
||||
$roomA = null;
|
||||
$roomB = null;
|
||||
foreach ($data as $room) {
|
||||
if ($room['token'] === 'abc123') $roomA = $room;
|
||||
if ($room['token'] === 'def456') $roomB = $room;
|
||||
}
|
||||
|
||||
$this->assertNotNull($roomA);
|
||||
$this->assertTrue($roomA['configured']);
|
||||
$this->assertTrue(isset($roomA['token']));
|
||||
$this->assertTrue(isset($roomA['name']));
|
||||
$this->assertTrue(isset($roomA['type_label']));
|
||||
|
||||
$this->assertNotNull($roomB);
|
||||
$this->assertFalse($roomB['configured']);
|
||||
}
|
||||
|
||||
public function testGetRoomsReturns500OnException(): void {
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('getAvailableTalkRooms')
|
||||
->willThrowException(new \Exception('DB connection failed'));
|
||||
|
||||
$controller = $this->makeController($talkService);
|
||||
|
||||
$response = $controller->getRooms();
|
||||
|
||||
$this->assertEquals(500, $response->getStatus());
|
||||
$this->assertStringContainsString('DB connection failed', $response->getData()['error']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// debug endpoint
|
||||
// =========================================================================
|
||||
|
||||
public function testDebugReturns403WhenDisabled(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(false);
|
||||
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$controller = new WebhookController(
|
||||
'nc_bot_webhooks',
|
||||
$this->createMock(IRequest::class),
|
||||
$talkService,
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(IAppManager::class),
|
||||
$this->createMock(IUserSession::class),
|
||||
$this->createMock(IGroupManager::class),
|
||||
$this->createMock(IConfig::class),
|
||||
$appConfig,
|
||||
$this->createMock(IClientService::class),
|
||||
$this->createMock(IShareManager::class),
|
||||
);
|
||||
|
||||
$response = $controller->debug();
|
||||
|
||||
$this->assertEquals(403, $response->getStatus());
|
||||
$this->assertStringContainsString('disabled', $response->getData()['error']);
|
||||
}
|
||||
|
||||
public function testDebugReturns200WhenEnabled(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(true);
|
||||
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('getBotUser')
|
||||
->willReturn(null);
|
||||
$talkService->method('hasBotPassword')
|
||||
->willReturn(false);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')
|
||||
->willReturn('admin');
|
||||
$user->method('isAdmin')
|
||||
->willReturn(true);
|
||||
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
$userSession->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$groupManager = $this->createMock(IGroupManager::class);
|
||||
$groupManager->method('isAdmin')
|
||||
->willReturn(true);
|
||||
|
||||
$config = $this->createMock(IConfig::class);
|
||||
$config->method('getSystemValueString')
|
||||
->willReturn('');
|
||||
|
||||
$controller = new WebhookController(
|
||||
'nc_bot_webhooks',
|
||||
$this->createMock(IRequest::class),
|
||||
$talkService,
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(IAppManager::class),
|
||||
$userSession,
|
||||
$groupManager,
|
||||
$config,
|
||||
$appConfig,
|
||||
$this->createMock(IClientService::class),
|
||||
$this->createMock(IShareManager::class),
|
||||
);
|
||||
|
||||
$response = $controller->debug();
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$data = $response->getData();
|
||||
$this->assertTrue($data['debug_enabled']);
|
||||
$this->assertEquals('admin', $data['user']);
|
||||
$this->assertTrue($data['user_is_admin']);
|
||||
}
|
||||
|
||||
public function testDebugNonAdminUser(): void {
|
||||
$appConfig = $this->createMock(IAppConfig::class);
|
||||
$appConfig->method('getValueBool')
|
||||
->willReturn(true);
|
||||
|
||||
$talkService = $this->createMock(TalkService::class);
|
||||
$talkService->method('getBotUser')
|
||||
->willReturn(null);
|
||||
$talkService->method('hasBotPassword')
|
||||
->willReturn(false);
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')
|
||||
->willReturn('regular_user');
|
||||
$user->method('isAdmin')
|
||||
->willReturn(false);
|
||||
|
||||
$userSession = $this->createMock(IUserSession::class);
|
||||
$userSession->method('getUser')
|
||||
->willReturn($user);
|
||||
|
||||
$groupManager = $this->createMock(IGroupManager::class);
|
||||
$groupManager->method('isAdmin')
|
||||
->willReturn(false);
|
||||
|
||||
$config = $this->createMock(IConfig::class);
|
||||
$config->method('getSystemValueString')
|
||||
->willReturn('');
|
||||
|
||||
$controller = new WebhookController(
|
||||
'nc_bot_webhooks',
|
||||
$this->createMock(IRequest::class),
|
||||
$talkService,
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(IAppManager::class),
|
||||
$userSession,
|
||||
$groupManager,
|
||||
$config,
|
||||
$appConfig,
|
||||
$this->createMock(IClientService::class),
|
||||
$this->createMock(IShareManager::class),
|
||||
);
|
||||
|
||||
$response = $controller->debug();
|
||||
|
||||
$this->assertEquals(200, $response->getStatus());
|
||||
$this->assertFalse($response->getData()['user_is_admin']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Nextcloud nc_bot_webhooks — PHPUnit bootstrap
|
||||
*
|
||||
* Minimal bootstrap that mocks the Talk app services so TalkService can be
|
||||
* instantiated without a running Nextcloud instance. Every constructor
|
||||
* dependency of TalkService gets a PHPUnit mock object.
|
||||
*/
|
||||
|
||||
// Load composer autoloader first
|
||||
$base = __DIR__ . '/../';
|
||||
$loader = $base . 'vendor/autoload.php';
|
||||
if (!file_exists($loader)) {
|
||||
fwrite(STDERR, "vendor/autoload.php not found — run `composer install`\n");
|
||||
exit(1);
|
||||
}
|
||||
require_once $loader;
|
||||
|
||||
// ── Minimal OCP / OC interface stubs ──────────────────────────
|
||||
// PHPUnit's mock generator needs the actual class/interface definitions
|
||||
// to inspect method signatures. We provide tiny stubs for anything that
|
||||
// is not already provided by composer / the real OCP packages.
|
||||
|
||||
// OCP\Http\Client\IClientService
|
||||
if (!interface_exists(\OCP\Http\Client\IClientService::class)) {
|
||||
namespace OCP\Http\Client {
|
||||
interface IClientService {
|
||||
public function getClient(): \OCP\Http\Client\IClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!interface_exists(\OCP\Http\Client\IClient::class)) {
|
||||
namespace OCP\Http\Client {
|
||||
interface IClient {
|
||||
public function get(string $url, array $options = []): \Psr\Http\Message\ResponseInterface;
|
||||
public function post(string $url, array $options = []): \Psr\Http\Message\ResponseInterface;
|
||||
public function delete(string $url): \Psr\Http\Message\ResponseInterface;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!interface_exists(\Psr\Http\Message\ResponseInterface::class)) {
|
||||
// PSR-7 — must already be loaded via composer; guard just in case
|
||||
namespace { /* nothing */ }
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Build a TalkService instance with all-mocked dependencies.
|
||||
*
|
||||
* Usage:
|
||||
* $builder = new TalkServiceMockBuilder();
|
||||
* $service = $builder->build(); // all defaults
|
||||
* $service = $builder->config(['app' => ['key' => 'val']])->build();
|
||||
* $service = $builder->botUser($botUser)->build();
|
||||
*/
|
||||
class TalkServiceMockBuilder {
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\Http\Client\IClientService $clientService;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\IConfig $config;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\IDBConnection $db;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\Files\IRootFolder $rootFolder;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\IRequest $request;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\IURLGenerator $urlGenerator;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\IUserManager $userManager;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\IUserSession $userSession;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \Psr\Log\LoggerInterface $logger;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCA\Talk\Manager $talkManager;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\Security\ICrypto $crypto;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCA\Talk\Model\AttendeeMapper $attendeeMapper;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCP\Share\IManager $shareManager;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCA\Talk\Service\ParticipantService $participantService;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCA\Talk\Chat\ChatManager $chatManager;
|
||||
private \PHPUnit\Framework\MockObject\MockObject| \OCA\Talk\TalkSession $talkSession;
|
||||
|
||||
public function __construct() {
|
||||
$this->clientService = $this->makeMock(\OCP\Http\Client\IClientService::class);
|
||||
$this->config = $this->makeMock(\OCP\IConfig::class);
|
||||
$this->db = $this->makeMock(\OCP\IDBConnection::class);
|
||||
$this->rootFolder = $this->makeMock(\OCP\Files\IRootFolder::class);
|
||||
$this->request = $this->makeMock(\OCP\IRequest::class);
|
||||
$this->urlGenerator = $this->makeMock(\OCP\IURLGenerator::class);
|
||||
$this->userManager = $this->makeMock(\OCP\IUserManager::class);
|
||||
$this->userSession = $this->makeMock(\OCP\IUserSession::class);
|
||||
$this->logger = $this->makeMock(\Psr\Log\LoggerInterface::class);
|
||||
$this->talkManager = $this->makeMock(\OCA\Talk\Manager::class);
|
||||
$this->crypto = $this->makeMock(\OCP\Security\ICrypto::class);
|
||||
$this->attendeeMapper = $this->makeMock(\OCA\Talk\Model\AttendeeMapper::class);
|
||||
$this->shareManager = $this->makeMock(\OCP\Share\IManager::class);
|
||||
$this->participantService = $this->makeMock(\OCA\Talk\Service\ParticipantService::class);
|
||||
$this->chatManager = $this->makeMock(\OCA\Talk\Chat\ChatManager::class);
|
||||
$this->talkSession = $this->makeMock(\OCA\Talk\TalkSession::class);
|
||||
}
|
||||
|
||||
private function makeMock(string $interface): \PHPUnit\Framework\MockObject\MockObject {
|
||||
return \PHPUnit\Framework\TestCase::getMockForTrait($interface);
|
||||
}
|
||||
|
||||
/** Configure the config mock so getAppValue / setAppValue work as expected. */
|
||||
public function config(array $appValues = []): self {
|
||||
// getMockForTrait returns a partial mock; we can still use willReturn / withConsecutive
|
||||
$mock = $this->config;
|
||||
if (is_object($mock) && method_exists($mock, 'expects')) {
|
||||
// We set expectations lazily inside each test; this is just a convenience
|
||||
// to pre-configure default return values.
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble and return a TalkService.
|
||||
*
|
||||
* @return \OCA\Ncbotwebhooks\Service\TalkService
|
||||
*/
|
||||
public function build(): \OCA\Ncbotwebhooks\Service\TalkService {
|
||||
return new \OCA\Ncbotwebhooks\Service\TalkService(
|
||||
$this->clientService,
|
||||
$this->config,
|
||||
$this->db,
|
||||
$this->rootFolder,
|
||||
$this->request,
|
||||
$this->urlGenerator,
|
||||
$this->userManager,
|
||||
$this->userSession,
|
||||
$this->logger,
|
||||
$this->talkManager,
|
||||
$this->crypto,
|
||||
$this->attendeeMapper,
|
||||
$this->shareManager,
|
||||
$this->participantService,
|
||||
$this->chatManager,
|
||||
$this->talkSession,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user