OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
137
tests/Unit/app/Domain/Help/Services/FirstTaskStepTest.php
Normal file
137
tests/Unit/app/Domain/Help/Services/FirstTaskStepTest.php
Normal 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']]],
|
||||
];
|
||||
}
|
||||
}
|
||||
96
tests/Unit/app/Domain/Help/Services/HelperTest.php
Normal file
96
tests/Unit/app/Domain/Help/Services/HelperTest.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user