shouldReceive('select')->once()->with(Mockery::on(function ($sql): bool { UpstreamReadOnlyQuery::assertReadOnlySql($sql); return str_contains($sql, 'TOP (12)') && str_contains($sql, 'flowerNameId') && ! str_contains($sql, "张'_%"); }), ["%张'!_!%![%", "%张'!_!%![%"])->andReturn([]); $factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class); $factory->shouldReceive('forSource')->once()->with('operations_replica')->andReturn($query); $roster = new FinanceRoster($factory); $this->assertSame([], $roster->search('')); $this->assertSame([], $roster->search("张'_%[")); } #[DataProvider('rosterCases')] public function test_roster_authorization(array $overrides, bool $expected): void { config(['dingtalk.finance_department_ids' => ['22848291', '994080505']]); config(['dingtalk.super_admin_user_ids' => $overrides['designated_ids'] ?? []]); $data = array_replace([ 'flowers' => [['Id' => 123, 'name' => '测试员工', 'flowerName' => '测试花名', 'deptId' => '55248182', 'status' => '', 'empStatus' => '正式']], 'employees' => [['active' => 1, 'dept_id_list' => '[55248182]']], 'registrations' => [['employment_status' => '3', 'department_ids' => '55248182', 'main_department_id' => '55248182']], 'resignation' => null, 'ancestors' => [['Id' => '55248182', 'parent_id' => '22848291', 'depth' => 0], ['Id' => '22848291', 'parent_id' => '1', 'depth' => 1]], ], $overrides); $query = Mockery::mock(UpstreamReadOnlyQuery::class); $query->shouldReceive('select')->andReturnUsing(function ($sql, $bindings) use ($data) { UpstreamReadOnlyQuery::assertReadOnlySql($sql); if (str_contains($sql, 'WITH lineage')) { $this->assertContains('55248182', $bindings); return $data['ancestors']; } $this->assertSame(['ding-fixture-id'], $bindings); return match (true) { str_contains($sql, 'hr_flowerName_book') => $data['flowers'], str_contains($sql, 'hr_employee_register') => $data['registrations'], str_contains($sql, 'hr_employee') => $data['employees'], }; }); $query->shouldReceive('first')->with(Mockery::on(function ($sql) { UpstreamReadOnlyQuery::assertReadOnlySql($sql); return str_contains($sql, 'hr_employee_dimission'); }), ['ding-fixture-id'])->andReturn($data['resignation']); $factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class); $factory->shouldReceive('forSource')->with('operations_replica')->andReturn($query); $result = (new FinanceRoster($factory))->findEligible('ding-fixture-id'); $this->assertSame($expected, $result !== null); } public static function rosterCases(): array { return [ 'designated active administrator outside finance' => [['designated_ids' => ['ding-fixture-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '1', 'depth' => 0]]], true], 'designation does not cover another identity' => [['designated_ids' => ['another-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '1', 'depth' => 0]]], false], 'designated administrator still requires active employment' => [['designated_ids' => ['ding-fixture-id'], 'employees' => [['active' => 0]]], false], 'designated administrator cannot bypass resignation' => [['designated_ids' => ['ding-fixture-id'], 'resignation' => ['userid' => 'ding-fixture-id']], false], 'designated administrator needs roster identity' => [['designated_ids' => ['ding-fixture-id'], 'flowers' => []], false], 'finance descendant' => [[], true], 'second finance root' => [['ancestors' => [['Id' => '994080505', 'parent_id' => '994121400', 'depth' => 1]]], true], 'concurrent finance membership' => [['employees' => [['active' => 1, 'dept_id_list' => '[100,55248182]']]], true], 'not in roster' => [['flowers' => []], false], 'same department name outside allowed roots' => [['ancestors' => [['Id' => '123456', 'parent_id' => '1', 'depth' => 0]]], false], 'resignation beats active status' => [['resignation' => ['userid' => 'ding-fixture-id']], false], 'numeric resigned status' => [['registrations' => [['employment_status' => '5']]], false], 'disabled employee' => [['employees' => [['active' => 0]]], false], 'flower-only member' => [['employees' => [], 'registrations' => []], true], 'ambiguous roster identity' => [['flowers' => [['Id' => 1], ['Id' => 2]]], false], 'broken department cycle' => [['ancestors' => [['Id' => '22848291', 'parent_id' => '22848291', 'depth' => 20]]], false], ]; } }