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,137 @@
<?php
namespace Unit\app\Domain\Help\Services;
use Leantime\Core\Exceptions\AuthorizationException;
use Leantime\Domain\Help\Services\FirstTaskStep;
use Leantime\Domain\Setting\Repositories\Setting as SettingRepository;
use Leantime\Domain\Tickets\Services\Tickets as TicketService;
use RuntimeException;
use Unit\TestCase;
/**
* Regression tests for the first-login onboarding dead-end in GH #3683.
*
* Readonly users have no TicketsPermissions::CREATE, so quickAddTicket() throws an
* AuthorizationException. That used to happen before the firstLoginCompleted flag was
* written, leaving the onboarding modal re-rendering forever with no way out. The flag
* write is the invariant here; the first task is only a convenience.
*/
class FirstTaskStepTest extends TestCase
{
use \Codeception\Test\Feature\Stub;
/** Captures every saveSetting() call as [key, value]. */
private array $savedSettings = [];
/** Captures every headline quickAddTicket() was called with. */
private array $createdHeadlines = [];
protected function setUp(): void
{
parent::setUp();
session(['userdata.id' => 1]);
$this->savedSettings = [];
$this->createdHeadlines = [];
}
/**
* Builds the step with a spying Setting repo and a Tickets service that either
* records the headline or throws the given exception.
*/
private function makeStep(?\Throwable $ticketFailure = null): FirstTaskStep
{
$settingsRepo = $this->make(SettingRepository::class, [
'saveSetting' => function ($key, $value) {
$this->savedSettings[] = [$key, $value];
return true;
},
]);
$ticketService = $this->make(TicketService::class, [
'quickAddTicket' => function ($params) use ($ticketFailure) {
if ($ticketFailure !== null) {
throw $ticketFailure;
}
$this->createdHeadlines[] = $params['headline'];
return 1;
},
]);
return new FirstTaskStep($settingsRepo, $ticketService);
}
/** The flag write, asserted as "written exactly once with true". */
private function assertOnboardingCompleted(): void
{
$this->assertSame(
[['user.1.firstLoginCompleted', true]],
$this->savedSettings,
'firstLoginCompleted must be persisted exactly once, regardless of ticket creation'
);
}
public function test_readonly_user_completes_onboarding_when_ticket_creation_is_denied(): void
{
$result = $this->makeStep(new AuthorizationException)->handle(['headline' => 'Water the plants']);
$this->assertTrue($result, 'handle() must report success even when the user cannot create tickets');
$this->assertOnboardingCompleted();
$this->assertSame([], $this->createdHeadlines);
}
public function test_unexpected_ticket_failure_still_completes_onboarding(): void
{
$result = $this->makeStep(new RuntimeException('database went away'))->handle(['headline' => 'Water the plants']);
$this->assertTrue($result);
$this->assertOnboardingCompleted();
}
public function test_permitted_user_creates_the_first_task_and_completes_onboarding(): void
{
$result = $this->makeStep()->handle(['headline' => 'Water the plants']);
$this->assertTrue($result);
$this->assertSame(['Water the plants'], $this->createdHeadlines);
$this->assertOnboardingCompleted();
}
public function test_headline_is_trimmed_before_the_task_is_created(): void
{
$this->makeStep()->handle(['headline' => ' Water the plants ']);
$this->assertSame(['Water the plants'], $this->createdHeadlines);
}
/**
* A blank, whitespace-only, missing or non-string headline must not create an empty
* task — but must still complete onboarding.
*
* @dataProvider blankHeadlineProvider
*/
public function test_blank_headline_skips_task_creation_but_completes_onboarding(array $params): void
{
$result = $this->makeStep()->handle($params);
$this->assertTrue($result);
$this->assertSame([], $this->createdHeadlines, 'No task should be created for a blank headline');
$this->assertOnboardingCompleted();
}
public static function blankHeadlineProvider(): array
{
return [
'empty string' => [['headline' => '']],
'whitespace only' => [['headline' => " \t "]],
'missing key' => [[]],
'null' => [['headline' => null]],
'array (malformed POST)' => [['headline' => ['nope']]],
];
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Unit\app\Domain\Help\Services;
use Leantime\Domain\Help\Services\Helper;
use Leantime\Domain\Setting\Repositories\Setting;
use Unit\TestCase;
/**
* Unit tests for the onboarding / modal orchestration extracted from the
* Help FirstLogin and ShowOnboardingDialog controllers into the Helper service.
*/
class HelperTest extends TestCase
{
use \Codeception\Test\Feature\Stub;
/**
* Builds a Helper service with a stubbed Setting repository.
*/
private function makeService(): Helper
{
return new Helper($this->make(Setting::class));
}
public function test_resolve_first_login_step_returns_end_step(): void
{
$step = $this->makeService()->resolveFirstLoginStep('end');
$this->assertTrue($step['isEnd']);
$this->assertSame('end', $step['key']);
$this->assertNull($step['next']);
$this->assertSame('help.firstLoginEnd', $step['template']);
}
public function test_handle_first_login_step_rejects_missing_step(): void
{
$result = $this->makeService()->handleFirstLoginStep([]);
$this->assertFalse($result['valid']);
$this->assertSame('', $result['next']);
}
public function test_handle_first_login_step_rejects_non_numeric_step(): void
{
$result = $this->makeService()->handleFirstLoginStep(['currentStep' => 'foo']);
$this->assertFalse($result['valid']);
$this->assertSame('', $result['next']);
}
public function test_handle_first_login_step_rejects_unknown_numeric_step(): void
{
$result = $this->makeService()->handleFirstLoginStep(['currentStep' => '999']);
$this->assertFalse($result['valid']);
$this->assertSame('', $result['next']);
}
public function test_get_helper_modal_by_route_returns_notfound_for_unknown_route(): void
{
$modal = $this->makeService()->getHelperModalByRoute('does.notExist');
$this->assertSame('notfound', $modal['template']);
}
public function test_mark_modal_seen_for_module_sanitizes_and_records_session(): void
{
session()->forget('usersettings.modals');
$template = $this->makeService()->markModalSeenForModule('<b>dashboard</b>');
$expected = htmlspecialchars('<b>dashboard</b>');
$this->assertSame($expected, $template);
$this->assertSame(1, session('usersettings.modals.'.$expected));
}
public function test_mark_modal_seen_for_route_resolves_template_and_records_session(): void
{
session()->forget('usersettings.modals');
$template = $this->makeService()->markModalSeenForRoute('dashboard.show');
$this->assertSame('projectDashboard', $template);
$this->assertSame(1, session('usersettings.modals.projectDashboard'));
}
public function test_mark_modal_seen_for_route_unknown_route_marks_notfound(): void
{
session()->forget('usersettings.modals');
$template = $this->makeService()->markModalSeenForRoute('does.notExist');
$this->assertSame('notfound', $template);
$this->assertSame(1, session('usersettings.modals.notfound'));
}
}