'测试同名人员']; 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()); } } }