86 lines
5.3 KiB
PHP
86 lines
5.3 KiB
PHP
<?php
|
||
|
||
namespace Tests\Unit\Auth;
|
||
|
||
use App\Services\Auth\FinanceRoster;
|
||
use Mockery;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||
use PHPUnit\Framework\Attributes\DataProvider;
|
||
use Tests\TestCase;
|
||
|
||
class FinanceRosterTest extends TestCase
|
||
{
|
||
public function test_directory_search_is_bounded_parameterized_and_escapes_wildcards(): void
|
||
{
|
||
$query = Mockery::mock(UpstreamReadOnlyQuery::class);
|
||
$query->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],
|
||
];
|
||
}
|
||
}
|