CaiWuXiTong/tests/Unit/Auth/FinanceRosterTest.php

123 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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("张'_%["));
}
public function test_finance_members_are_loaded_from_descendant_departments(): void
{
config(['dingtalk.finance_department_ids' => ['22848291', '994080505']]);
$query = Mockery::mock(UpstreamReadOnlyQuery::class);
$query->shouldReceive('select')->once()->with(
Mockery::on(static function (string $sql): bool {
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
return str_contains($sql, 'WITH finance_groups')
&& str_contains($sql, "Id IN (?,?)")
&& str_contains($sql, 'STRING_SPLIT')
&& str_contains($sql, "REPLACE(value, '\"', '')");
}),
['22848291', '994080505'],
)->andReturn([
['userid' => '1001', 'name' => '张宏', 'flowerName' => '人参'],
['userid' => '1002', 'name' => '吕庭', 'flowerName' => '江陵'],
]);
$factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class);
$factory->shouldReceive('forSourceWithTimeout')
->once()->with('operations_replica', 60)->andReturn($query);
$this->assertSame([
['userid' => '1001', 'name' => '人参'],
['userid' => '1002', 'name' => '江陵'],
], (new FinanceRoster($factory))->financeMembers());
}
#[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'] ?? []]);
config(['dingtalk.limited_login_user_ids' => $overrides['limited_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 [
'limited current employee outside finance' => [['limited_ids' => ['ding-fixture-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '1', 'depth' => 0]]], true],
'limited grant is exact identity only' => [['limited_ids' => ['another-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '1', 'depth' => 0]]], false],
'limited grant still requires unique roster identity' => [['limited_ids' => ['ding-fixture-id'], 'flowers' => []], false],
'limited grant rejects duplicate roster identity' => [['limited_ids' => ['ding-fixture-id'], 'flowers' => [['Id' => 1], ['Id' => 2]]], false],
'limited grant cannot bypass resignation' => [['limited_ids' => ['ding-fixture-id'], 'resignation' => ['userid' => 'ding-fixture-id']], false],
'limited grant cannot bypass disabled employment' => [['limited_ids' => ['ding-fixture-id'], 'employees' => [['active' => 0]]], false],
'limited grant cannot bypass resigned registration' => [['limited_ids' => ['ding-fixture-id'], 'registrations' => [['employment_status' => '5']]], false],
'limited grant cannot bypass broken organization' => [['limited_ids' => ['ding-fixture-id'], 'ancestors' => [['Id' => '55248182', 'parent_id' => '55248182', 'depth' => 20]]], false],
'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],
];
}
}