OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}