OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
116
tests/Unit/app/Domain/Notifications/NotificationCategoryTest.php
Normal file
116
tests/Unit/app/Domain/Notifications/NotificationCategoryTest.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Unit\app\Domain\Notifications;
|
||||
|
||||
use Leantime\Domain\Notifications\Models\Notification;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NotificationCategoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider moduleToCategoryProvider
|
||||
*/
|
||||
public function test_get_category_for_module_return_correct_category(string $module, ?string $expectedCategory): void
|
||||
{
|
||||
$this->assertSame($expectedCategory, Notification::getCategoryForModule($module));
|
||||
}
|
||||
|
||||
public static function moduleToCategoryProvider(): array
|
||||
{
|
||||
return [
|
||||
'tickets maps to tasks' => ['tickets', 'tasks'],
|
||||
'comments maps to comments' => ['comments', 'comments'],
|
||||
'goalcanvas maps to goals' => ['goalcanvas', 'goals'],
|
||||
'ideas maps to ideas' => ['ideas', 'ideas'],
|
||||
'projects maps to projects' => ['projects', 'projects'],
|
||||
'leancanvas maps to boards' => ['leancanvas', 'boards'],
|
||||
'swotcanvas maps to boards' => ['swotcanvas', 'boards'],
|
||||
'retroscanvas maps to boards' => ['retroscanvas', 'boards'],
|
||||
'cpcanvas maps to boards' => ['cpcanvas', 'boards'],
|
||||
'unknown module returns null' => ['someOtherModule', null],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_all_categories_have_required_structure(): void
|
||||
{
|
||||
$categories = Notification::NOTIFICATION_CATEGORIES;
|
||||
|
||||
$this->assertArrayHasKey('tasks', $categories);
|
||||
$this->assertArrayHasKey('comments', $categories);
|
||||
$this->assertArrayHasKey('goals', $categories);
|
||||
$this->assertArrayHasKey('ideas', $categories);
|
||||
$this->assertArrayHasKey('projects', $categories);
|
||||
$this->assertArrayHasKey('boards', $categories);
|
||||
$this->assertCount(6, $categories);
|
||||
|
||||
// Each category must have 'modules' and 'description' keys
|
||||
foreach ($categories as $key => $config) {
|
||||
$this->assertArrayHasKey('modules', $config, "Category '$key' missing 'modules' key");
|
||||
$this->assertArrayHasKey('description', $config, "Category '$key' missing 'description' key");
|
||||
$this->assertIsArray($config['modules'], "Category '$key' modules must be an array");
|
||||
$this->assertIsString($config['description'], "Category '$key' description must be a string");
|
||||
$this->assertNotEmpty($config['description'], "Category '$key' description must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public function test_goalcanvas_is_not_boards(): void
|
||||
{
|
||||
// goalcanvas specifically maps to 'goals', NOT 'boards'
|
||||
$this->assertSame('goals', Notification::getCategoryForModule('goalcanvas'));
|
||||
$this->assertNotSame('boards', Notification::getCategoryForModule('goalcanvas'));
|
||||
}
|
||||
|
||||
public function test_get_category_keys_returns_all_keys(): void
|
||||
{
|
||||
$keys = Notification::getCategoryKeys();
|
||||
$this->assertCount(6, $keys);
|
||||
$this->assertContains('tasks', $keys);
|
||||
$this->assertContains('comments', $keys);
|
||||
$this->assertContains('goals', $keys);
|
||||
$this->assertContains('ideas', $keys);
|
||||
$this->assertContains('projects', $keys);
|
||||
$this->assertContains('boards', $keys);
|
||||
}
|
||||
|
||||
public function test_relevance_levels_are_defined(): void
|
||||
{
|
||||
$this->assertSame('all', Notification::RELEVANCE_ALL);
|
||||
$this->assertSame('my_work', Notification::RELEVANCE_MY_WORK);
|
||||
$this->assertSame('muted', Notification::RELEVANCE_MUTED);
|
||||
|
||||
$levels = Notification::RELEVANCE_LEVELS;
|
||||
$this->assertCount(3, $levels);
|
||||
$this->assertArrayHasKey('all', $levels);
|
||||
$this->assertArrayHasKey('my_work', $levels);
|
||||
$this->assertArrayHasKey('muted', $levels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider relevanceLevelValidationProvider
|
||||
*/
|
||||
public function test_is_valid_relevance_level(string $level, bool $expected): void
|
||||
{
|
||||
$this->assertSame($expected, Notification::isValidRelevanceLevel($level));
|
||||
}
|
||||
|
||||
public static function relevanceLevelValidationProvider(): array
|
||||
{
|
||||
return [
|
||||
'all is valid' => ['all', true],
|
||||
'my_work is valid' => ['my_work', true],
|
||||
'muted is valid' => ['muted', true],
|
||||
'empty is invalid' => ['', false],
|
||||
'random string is invalid' => ['something_else', false],
|
||||
'ALL uppercase is invalid' => ['ALL', false],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_notification_model_has_action_property(): void
|
||||
{
|
||||
$notification = new Notification;
|
||||
$this->assertSame('', $notification->action);
|
||||
|
||||
$notification->action = 'created';
|
||||
$this->assertSame('created', $notification->action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Unit\app\Domain\Notifications\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Leantime\Core\Language as LanguageCore;
|
||||
use Leantime\Domain\Notifications\Models\Notification as NotificationModel;
|
||||
use Leantime\Domain\Notifications\Services\Messengers;
|
||||
use Leantime\Domain\Setting\Repositories\Setting as SettingRepository;
|
||||
use Unit\TestCase;
|
||||
|
||||
class MessengersServiceTest extends TestCase
|
||||
{
|
||||
use \Codeception\Test\Feature\Stub;
|
||||
|
||||
private function makeNotification(int $projectId = 1, string $message = 'Test notification'): NotificationModel
|
||||
{
|
||||
$notification = new NotificationModel;
|
||||
$notification->projectId = $projectId;
|
||||
$notification->message = $message;
|
||||
$notification->url = ['url' => 'https://example.com/ticket/123'];
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
public function test_send_notification_to_messengers_skips_telegram_when_unconfigured(): void
|
||||
{
|
||||
$posted = false;
|
||||
$client = $this->make(Client::class, [
|
||||
'post' => function () use (&$posted) {
|
||||
$posted = true;
|
||||
|
||||
return new Response(200);
|
||||
},
|
||||
]);
|
||||
|
||||
$settingRepo = $this->make(SettingRepository::class, [
|
||||
'getSetting' => fn () => false,
|
||||
]);
|
||||
|
||||
$language = $this->make(LanguageCore::class);
|
||||
|
||||
$messengers = new Messengers($client, $settingRepo, $language);
|
||||
$messengers->sendNotificationToMessengers($this->makeNotification(), 'Test Project', ['telegram']);
|
||||
|
||||
$this->assertFalse($posted);
|
||||
}
|
||||
|
||||
public function test_telegram_webhook_returns_false_when_hook_missing_required_fields(): void
|
||||
{
|
||||
$posted = false;
|
||||
$client = $this->make(Client::class, [
|
||||
'post' => function () use (&$posted) {
|
||||
$posted = true;
|
||||
|
||||
return new Response(200);
|
||||
},
|
||||
]);
|
||||
|
||||
$settingRepo = $this->make(SettingRepository::class, [
|
||||
'getSetting' => fn () => serialize([
|
||||
'telegramBotToken' => '12345:ABC',
|
||||
'telegramChatId' => '',
|
||||
'telegramTopicId' => '',
|
||||
]),
|
||||
]);
|
||||
|
||||
$language = $this->make(LanguageCore::class);
|
||||
|
||||
$messengers = new Messengers($client, $settingRepo, $language);
|
||||
$messengers->sendNotificationToMessengers($this->makeNotification(), 'Test Project', ['telegram']);
|
||||
|
||||
$this->assertFalse($posted);
|
||||
}
|
||||
|
||||
public function test_telegram_webhook_posts_to_api_and_succeeds(): void
|
||||
{
|
||||
$capturedUrl = null;
|
||||
$capturedOptions = null;
|
||||
|
||||
$client = $this->make(Client::class, [
|
||||
'post' => function ($url, $options) use (&$capturedUrl, &$capturedOptions) {
|
||||
$capturedUrl = $url;
|
||||
$capturedOptions = $options;
|
||||
|
||||
return new Response(200, [], json_encode(['ok' => true]));
|
||||
},
|
||||
]);
|
||||
|
||||
$settingRepo = $this->make(SettingRepository::class, [
|
||||
'getSetting' => fn () => serialize([
|
||||
'telegramBotToken' => '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
|
||||
'telegramChatId' => '987654321',
|
||||
'telegramTopicId' => '',
|
||||
]),
|
||||
]);
|
||||
|
||||
$language = $this->make(LanguageCore::class);
|
||||
|
||||
$messengers = new Messengers($client, $settingRepo, $language);
|
||||
$messengers->sendNotificationToMessengers($this->makeNotification(1, 'Task created'), 'Acme Project', ['telegram']);
|
||||
|
||||
$this->assertSame('https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/sendMessage', $capturedUrl);
|
||||
$this->assertArrayHasKey('json', $capturedOptions);
|
||||
$this->assertSame('987654321', $capturedOptions['json']['chat_id']);
|
||||
$this->assertStringContainsString('<b>Acme Project</b>', $capturedOptions['json']['text']);
|
||||
$this->assertStringContainsString('Task created', $capturedOptions['json']['text']);
|
||||
$this->assertStringContainsString('https://example.com/ticket/123', $capturedOptions['json']['text']);
|
||||
$this->assertSame('HTML', $capturedOptions['json']['parse_mode']);
|
||||
$this->assertArrayNotHasKey('message_thread_id', $capturedOptions['json']);
|
||||
}
|
||||
|
||||
public function test_telegram_webhook_includes_message_thread_id_when_topic_id_provided(): void
|
||||
{
|
||||
$capturedOptions = null;
|
||||
|
||||
$client = $this->make(Client::class, [
|
||||
'post' => function ($url, $options) use (&$capturedOptions) {
|
||||
$capturedOptions = $options;
|
||||
|
||||
return new Response(200, [], json_encode(['ok' => true]));
|
||||
},
|
||||
]);
|
||||
|
||||
$settingRepo = $this->make(SettingRepository::class, [
|
||||
'getSetting' => fn () => serialize([
|
||||
'telegramBotToken' => '123456:ABC-DEF',
|
||||
'telegramChatId' => '-1001234567890',
|
||||
'telegramTopicId' => '42',
|
||||
]),
|
||||
]);
|
||||
|
||||
$language = $this->make(LanguageCore::class);
|
||||
|
||||
$messengers = new Messengers($client, $settingRepo, $language);
|
||||
$messengers->sendNotificationToMessengers($this->makeNotification(), 'Test Project', ['telegram']);
|
||||
|
||||
$this->assertArrayHasKey('json', $capturedOptions);
|
||||
$this->assertSame('-1001234567890', $capturedOptions['json']['chat_id']);
|
||||
$this->assertSame(42, $capturedOptions['json']['message_thread_id']);
|
||||
}
|
||||
|
||||
public function test_telegram_webhook_catches_guzzle_exception_and_returns_false(): void
|
||||
{
|
||||
$client = $this->make(Client::class, [
|
||||
'post' => function () {
|
||||
throw new RequestException('API connection error', new Request('POST', 'test'));
|
||||
},
|
||||
]);
|
||||
|
||||
$settingRepo = $this->make(SettingRepository::class, [
|
||||
'getSetting' => fn () => serialize([
|
||||
'telegramBotToken' => '123456:ABC-DEF',
|
||||
'telegramChatId' => '987654321',
|
||||
'telegramTopicId' => '',
|
||||
]),
|
||||
]);
|
||||
|
||||
$language = $this->make(LanguageCore::class);
|
||||
|
||||
$messengers = new Messengers($client, $settingRepo, $language);
|
||||
|
||||
$reflectedMethod = new \ReflectionMethod($messengers, 'telegramWebhook');
|
||||
$reflectedMethod->setAccessible(true);
|
||||
$result = $reflectedMethod->invoke($messengers, $this->makeNotification());
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Unit\app\Domain\Notifications\Services;
|
||||
|
||||
use Leantime\Core\Language as LanguageCore;
|
||||
use Leantime\Domain\Notifications\Repositories\Notifications as NotificationRepository;
|
||||
use Leantime\Domain\Notifications\Services\Notifications;
|
||||
use Leantime\Domain\Users\Repositories\Users as UserRepository;
|
||||
use Unit\TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for the flash-notification orchestration extracted from the
|
||||
* Notifications GetLatestGrowl controller into the Notifications service.
|
||||
*/
|
||||
class NotificationsServiceTest extends TestCase
|
||||
{
|
||||
use \Codeception\Test\Feature\Stub;
|
||||
|
||||
/**
|
||||
* Builds a Notifications service with stubbed dependencies. The
|
||||
* consumeFlashNotification logic only touches the session, so the
|
||||
* collaborators just need to exist.
|
||||
*/
|
||||
private function makeService(): Notifications
|
||||
{
|
||||
return new Notifications(
|
||||
$this->make(NotificationRepository::class),
|
||||
$this->make(UserRepository::class),
|
||||
$this->make(LanguageCore::class),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_consume_flash_notification_returns_null_when_empty(): void
|
||||
{
|
||||
session(['notification' => '']);
|
||||
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertNull($service->consumeFlashNotification());
|
||||
}
|
||||
|
||||
public function test_consume_flash_notification_returns_payload_and_clears_session(): void
|
||||
{
|
||||
session(['notification' => 'Saved!']);
|
||||
session(['notificationType' => 'success']);
|
||||
session(['eventId' => 'ticket-42']);
|
||||
|
||||
$service = $this->makeService();
|
||||
|
||||
$payload = $service->consumeFlashNotification();
|
||||
|
||||
$this->assertSame([
|
||||
'notification' => 'Saved!',
|
||||
'type' => 'success',
|
||||
'eventId' => 'ticket-42',
|
||||
], $payload);
|
||||
|
||||
// Read-once: session keys are cleared after consumption.
|
||||
$this->assertSame('', session('notification'));
|
||||
$this->assertSame('', session('notificationType'));
|
||||
$this->assertSame('', session('eventId'));
|
||||
}
|
||||
|
||||
public function test_consume_flash_notification_defaults_missing_type_and_event(): void
|
||||
{
|
||||
session()->forget('notificationType');
|
||||
session()->forget('eventId');
|
||||
session(['notification' => 'Hello']);
|
||||
|
||||
$service = $this->makeService();
|
||||
|
||||
$payload = $service->consumeFlashNotification();
|
||||
|
||||
$this->assertSame([
|
||||
'notification' => 'Hello',
|
||||
'type' => '',
|
||||
'eventId' => '',
|
||||
], $payload);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// markRead() — session-based JSON-RPC wrapper (must target the session user)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
public function test_mark_read_all_targets_the_session_user(): void
|
||||
{
|
||||
session(['userdata' => ['id' => 42]]);
|
||||
|
||||
$capturedUserId = null;
|
||||
$repo = $this->make(NotificationRepository::class, [
|
||||
'markAllNotificationRead' => function ($userId, ...$rest) use (&$capturedUserId) {
|
||||
$capturedUserId = $userId;
|
||||
|
||||
return true;
|
||||
},
|
||||
]);
|
||||
|
||||
$service = new Notifications(
|
||||
$repo,
|
||||
$this->make(UserRepository::class),
|
||||
$this->make(LanguageCore::class),
|
||||
);
|
||||
|
||||
$this->assertTrue($service->markRead('all'));
|
||||
$this->assertSame(42, $capturedUserId, "markRead('all') must use the session user");
|
||||
}
|
||||
|
||||
public function test_mark_read_specific_id_delegates_to_repo(): void
|
||||
{
|
||||
session(['userdata' => ['id' => 7]]);
|
||||
|
||||
$calledWith = null;
|
||||
$repo = $this->make(NotificationRepository::class, [
|
||||
'markNotificationRead' => function ($id, ...$rest) use (&$calledWith) {
|
||||
$calledWith = $id;
|
||||
|
||||
return true;
|
||||
},
|
||||
]);
|
||||
|
||||
$service = new Notifications(
|
||||
$repo,
|
||||
$this->make(UserRepository::class),
|
||||
$this->make(LanguageCore::class),
|
||||
);
|
||||
|
||||
$this->assertTrue($service->markRead(5));
|
||||
$this->assertSame(5, $calledWith);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user