CaiWuXiTong/tests/Feature/SystemSettings/WorkflowReviewersTest.php

154 lines
8.3 KiB
PHP

<?php
namespace Tests\Feature\SystemSettings;
use App\Models\Auth\DingTalkIdentity;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use App\Models\Common\Company;
use App\Services\Auth\FinanceRoster;
use App\Support\CourierOnboardingReviewers;
use Illuminate\Support\Facades\DB;
use Tests\Feature\FeatureTestCase;
class WorkflowReviewersTest extends FeatureTestCase
{
private mixed $rosterOutcome = ['display_name' => '测试同名人员'];
private bool $searchFails = false;
protected function setUp(): void
{
parent::setUp();
$this->mock(FinanceRoster::class, function ($mock): void {
$mock->shouldReceive('findEligible')->andReturnUsing(function () {
if ($this->rosterOutcome instanceof \Throwable) throw $this->rosterOutcome;
return $this->rosterOutcome;
});
$mock->shouldReceive('search')->andReturnUsing(function () {
if ($this->searchFails) throw new \RuntimeException('secret host');
return [
['flowerNameId' => 'ding-one', 'name' => '测试人员', 'flowerName' => '测试花名'],
['flowerNameId' => 'ding-unbound', 'name' => '未绑定人员', 'flowerName' => ''],
];
});
});
}
private function person(string $id, string $name = '测试同名人员'): User
{
$user = User::create(['name' => $name, 'email' => $id . '@example.test', 'password' => 'password',
'locale' => 'zh-CN', 'enabled' => true, 'landing_page' => 'dashboard', 'created_from' => 'core::test']);
$user->companies()->attach($this->company->id);
$user->roles()->attach(Role::where('name', 'user')->firstOrFail()->id);
$user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
DingTalkIdentity::create(['user_id' => $user->id, 'union_id' => 'union-' . $id, 'dingtalk_user_id' => $id,
'bound_at' => now()]);
return $user;
}
private function eligibleRoster(): void
{
$this->rosterOutcome = ['display_name' => '测试同名人员'];
}
public function test_admin_saves_distinct_identities_and_audits_changes_without_granting_admin_roles(): void
{
$one = $this->person('ding-one');
$two = $this->person('ding-two');
$this->eligibleRoster();
$this->loginAs()->get(route('system-settings.workflow-reviewers.index'))->assertOk()->assertSeeText('尚未配置审核人员');
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-one', 'ding-two']])
->assertRedirect(route('system-settings.workflow-reviewers.index'));
$settings = app(CourierOnboardingReviewers::class)->settings();
$this->assertSame(['ding-one', 'ding-two'], array_column($settings['final'], 'dingtalk_user_id'));
$this->assertSame('user', $two->roles()->first()->name);
$this->assertSame(1, count(app(CourierOnboardingReviewers::class)->history()));
// A real next request reloads all settings, including the audit namespace.
$this->company->makeCurrent(true);
$this->get(route('system-settings.workflow-reviewers.index'))->assertOk()->assertSeeText('配置变更记录');
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-two'], 'final' => ['ding-one']]);
$history = app(CourierOnboardingReviewers::class)->history();
$this->assertCount(2, $history);
$this->assertSame('ding-one', $history[0]['before']['first'][0]['dingtalk_user_id']);
$this->assertSame($this->user->id, $history[0]['actor_id']);
}
public function test_regular_user_cannot_read_search_or_save_configuration(): void
{
$person = $this->person('ding-one');
$this->withExceptionHandling()->loginAs($person, $this->company)
->get(route('system-settings.workflow-reviewers.index'))->assertForbidden();
$this->getJson(route('system-settings.workflow-reviewers.search', ['q' => '测试']))->assertForbidden();
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-one']])->assertForbidden();
}
public function test_directory_results_distinguish_bound_and_unbound_people_and_fail_safely(): void
{
$this->person('ding-one');
$this->loginAs()->getJson(route('system-settings.workflow-reviewers.search', ['q' => '测试']))
->assertOk()->assertJsonPath('people.0.selectable', true)->assertJsonPath('people.1.selectable', false)
->assertHeader('Cache-Control', 'no-store, private');
$this->searchFails = true;
$this->getJson(route('system-settings.workflow-reviewers.search', ['q' => '测试']))
->assertStatus(503)->assertDontSee('secret host');
}
public function test_save_rejects_missing_duplicate_same_person_inactive_and_unbound_candidates(): void
{
$one = $this->person('ding-one');
$two = $this->person('ding-two');
$this->eligibleRoster();
$this->withExceptionHandling()->loginAs()->patch(route('system-settings.workflow-reviewers.update'), ['first' => [], 'final' => []])
->assertSessionHasErrors(['first', 'final']);
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-one']])
->assertSessionHasErrors('final');
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one', 'ding-one'], 'final' => ['ding-two']])
->assertSessionHasErrors('first.0');
$two->update(['enabled' => false]);
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-two']])
->assertSessionHasErrors('final');
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['not-bound']])
->assertSessionHasErrors('final');
$this->assertSame([], app(CourierOnboardingReviewers::class)->settings()['first']);
$this->assertSame([], app(CourierOnboardingReviewers::class)->history());
}
public function test_save_rejects_departed_or_failed_roster_and_revoked_page_access(): void
{
$this->person('ding-one');
$this->person('ding-two');
$this->rosterOutcome = null;
$this->withExceptionHandling()->loginAs()->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-two']])
->assertSessionHasErrors('first');
$this->rosterOutcome = new \RuntimeException('private connection');
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-two']])
->assertSessionHasErrors('approval');
$this->eligibleRoster();
setting()->set('system.sidebar_permissions', json_encode(['configured' => ['role_user'], 'subjects' => ['role_user' => []]]));
setting()->save();
$this->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-two']])
->assertSessionHasErrors('first');
$this->assertSame([], app(CourierOnboardingReviewers::class)->history());
}
public function test_company_configuration_and_history_are_isolated(): void
{
$one = $this->person('ding-one');
$this->person('ding-two');
$this->eligibleRoster();
$this->loginAs()->patch(route('system-settings.workflow-reviewers.update'), ['first' => ['ding-one'], 'final' => ['ding-two']]);
$other = Company::create(['enabled' => true]);
$this->user->companies()->attach($other->id);
$this->loginAs($this->user, $other);
$this->assertSame([], app(CourierOnboardingReviewers::class)->settings()['first']);
$this->assertSame([], app(CourierOnboardingReviewers::class)->history());
try {
app(CourierOnboardingReviewers::class)->save(['first' => ['ding-one'], 'final' => ['ding-two']], $this->user);
$this->fail('其他公司人员不能被选为本公司审核人员');
} catch (\Illuminate\Validation\ValidationException $e) {
$this->assertArrayHasKey('first', $e->errors());
}
}
}