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,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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user