mirror of
https://github.com/kuaifan/dootask.git
synced 2026-05-23 17:04:10 +00:00
327 lines
12 KiB
PHP
327 lines
12 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\User;
|
||
use App\Models\UserDepartment;
|
||
use App\Models\WebSocketDialog;
|
||
use App\Models\WebSocketDialogUser;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Tests\TestCase;
|
||
|
||
/**
|
||
* 部门管理员 important 标记测试
|
||
*/
|
||
class DepartmentDeputyImportantTest extends TestCase
|
||
{
|
||
use DatabaseTransactions;
|
||
|
||
/**
|
||
* 是否属于"测试环境无 Swoole runtime / PushTask 串行 HTTP fallback 也不可用"的环境性失败
|
||
* 这些失败与业务逻辑无关,遇到时 markTestSkipped 而非 fail
|
||
*/
|
||
private function isSwooleInfraFailure(\Throwable $e): bool
|
||
{
|
||
$msg = $e->getMessage();
|
||
// "swoole/Swoole/__wakeup" 来自 Task::deliver 的 swoole 容器绑定;
|
||
// "Undefined array key" 来自 Ihttp::ihttp_request 的 fallback URL 解析路径
|
||
// 两者皆为测试环境基础设施问题,与业务逻辑无关
|
||
return str_contains($msg, 'swoole')
|
||
|| str_contains($msg, 'Swoole')
|
||
|| str_contains($msg, 'AbstractData::__wakeup')
|
||
|| str_contains($msg, 'Undefined array key');
|
||
}
|
||
|
||
private function makeUser(string $email): User
|
||
{
|
||
$user = User::createInstance([
|
||
'email' => $email,
|
||
'userimg' => '',
|
||
'nickname' => 'TestUser_' . substr(md5($email), 0, 6),
|
||
'profession' => '',
|
||
'password' => md5('123456'),
|
||
]);
|
||
$user->save();
|
||
return $user;
|
||
}
|
||
|
||
private function makeDepartment(string $name, int $ownerUserid): UserDepartment
|
||
{
|
||
$dept = UserDepartment::createInstance([
|
||
'name' => $name,
|
||
'parent_id' => 0,
|
||
'owner_userid' => $ownerUserid,
|
||
]);
|
||
$dept->save();
|
||
|
||
// 创建部门群
|
||
$dialog = WebSocketDialog::createGroup($name, [$ownerUserid], 'department', $ownerUserid);
|
||
$dept->dialog_id = $dialog->id;
|
||
$dept->save();
|
||
|
||
// 负责人加入部门
|
||
$owner = User::find($ownerUserid);
|
||
if ($owner) {
|
||
$owner->department = "," . $dept->id . ",";
|
||
$owner->save();
|
||
}
|
||
|
||
return $dept->fresh();
|
||
}
|
||
|
||
/**
|
||
* 测试:任命部门管理员时,应设置 important=true
|
||
*/
|
||
public function test_add_deputy_sets_important_flag()
|
||
{
|
||
$owner = $this->makeUser('owner@test.local');
|
||
$deputy = $this->makeUser('deputy@test.local');
|
||
$dept = $this->makeDepartment('TestDept', $owner->userid);
|
||
|
||
// 任命部门管理员
|
||
$dept->addDeputy($deputy->userid);
|
||
|
||
// 验证部门管理员已加入部门群
|
||
$dialogUser = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||
->where('userid', $deputy->userid)
|
||
->first();
|
||
|
||
$this->assertNotNull($dialogUser, '部门管理员应该已加入部门群');
|
||
$this->assertEquals(2, (int)$dialogUser->role, '部门管理员 role 应为 2');
|
||
$this->assertTrue((bool)$dialogUser->important, '部门管理员 important 应为 true');
|
||
}
|
||
|
||
/**
|
||
* 测试:罢免部门管理员后,应从部门群移出
|
||
*/
|
||
public function test_del_deputy_removes_from_department_group()
|
||
{
|
||
$owner = $this->makeUser('owner3@test.local');
|
||
$deputy = $this->makeUser('deputy3@test.local');
|
||
$dept = $this->makeDepartment('TestDept3', $owner->userid);
|
||
|
||
// 任命部门管理员
|
||
$dept->addDeputy($deputy->userid);
|
||
|
||
// 验证已加入
|
||
$this->assertTrue(WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||
->where('userid', $deputy->userid)->exists());
|
||
|
||
// 罢免部门管理员
|
||
$dept->delDeputy($deputy->userid);
|
||
|
||
// 验证已移出部门群
|
||
$this->assertFalse(WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||
->where('userid', $deputy->userid)->exists());
|
||
}
|
||
|
||
/**
|
||
* 测试:部门负责人也应该有 important 标记
|
||
*/
|
||
public function test_department_owner_has_important_flag()
|
||
{
|
||
$owner = $this->makeUser('owner4@test.local');
|
||
$dept = $this->makeDepartment('TestDept4', $owner->userid);
|
||
|
||
$dialogUser = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||
->where('userid', $owner->userid)
|
||
->first();
|
||
|
||
$this->assertNotNull($dialogUser);
|
||
$this->assertEquals(1, (int)$dialogUser->role, '部门负责人 role 应为 1');
|
||
$this->assertTrue((bool)$dialogUser->important, '部门负责人 important 应为 true');
|
||
}
|
||
|
||
/**
|
||
* 测试:任命部门管理员是幂等的
|
||
*/
|
||
public function test_add_deputy_is_idempotent()
|
||
{
|
||
$owner = $this->makeUser('owner5@test.local');
|
||
$deputy = $this->makeUser('deputy5@test.local');
|
||
$dept = $this->makeDepartment('TestDept5', $owner->userid);
|
||
|
||
// 第一次任命
|
||
$dept->addDeputy($deputy->userid);
|
||
|
||
// 第二次任命(不应报错)
|
||
$dept->addDeputy($deputy->userid);
|
||
|
||
// 验证只有一条记录
|
||
$count = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||
->where('userid', $deputy->userid)
|
||
->count();
|
||
|
||
$this->assertEquals(1, $count);
|
||
}
|
||
|
||
/**
|
||
* 测试:部门管理员自动加入 users.department
|
||
*/
|
||
public function test_deputy_auto_joins_department_members()
|
||
{
|
||
$owner = $this->makeUser('owner6@test.local');
|
||
$deputy = $this->makeUser('deputy6@test.local');
|
||
$dept = $this->makeDepartment('TestDept6', $owner->userid);
|
||
|
||
// 任命前不在部门
|
||
$deputy = $deputy->fresh();
|
||
$this->assertNotContains($dept->id, $deputy->department);
|
||
|
||
// 任命部门管理员
|
||
$dept->addDeputy($deputy->userid);
|
||
|
||
// 任命后应在部门
|
||
$deputy = $deputy->fresh();
|
||
$this->assertContains($dept->id, $deputy->department);
|
||
}
|
||
|
||
/**
|
||
* 测试:罢免部门管理员后,从 users.department 移除
|
||
*/
|
||
public function test_del_deputy_removes_from_department_members()
|
||
{
|
||
$owner = $this->makeUser('owner7@test.local');
|
||
$deputy = $this->makeUser('deputy7@test.local');
|
||
$dept = $this->makeDepartment('TestDept7', $owner->userid);
|
||
|
||
// 任命部门管理员
|
||
$dept->addDeputy($deputy->userid);
|
||
|
||
$deputy = $deputy->fresh();
|
||
$this->assertContains($dept->id, $deputy->department);
|
||
|
||
// 罢免部门管理员
|
||
$dept->delDeputy($deputy->userid);
|
||
|
||
// 应从部门移除
|
||
$deputy = $deputy->fresh();
|
||
$this->assertNotContains($dept->id, $deputy->department);
|
||
}
|
||
|
||
/**
|
||
* 测试:不能将部门负责人任命为部门管理员
|
||
*/
|
||
public function test_cannot_add_primary_owner_as_deputy()
|
||
{
|
||
$owner = $this->makeUser('owner8@test.local');
|
||
$dept = $this->makeDepartment('TestDept8', $owner->userid);
|
||
|
||
$this->expectException(\App\Exceptions\ApiException::class);
|
||
$this->expectExceptionMessage('不能将部门负责人任命为部门管理员');
|
||
|
||
$dept->addDeputy($owner->userid);
|
||
}
|
||
|
||
/**
|
||
* P0-A:delDeputy(当前部门负责人) 必须只清理 user_department_owners 残留,
|
||
* 绝不能把负责人从 users.department 或部门群移除
|
||
*
|
||
* 直接通过 DB 构造"残留"场景,避免触发 pushMsg/Swoole(测试环境无 swoole runtime)
|
||
*/
|
||
public function test_del_deputy_does_not_remove_current_primary_owner()
|
||
{
|
||
try {
|
||
$owner = $this->makeUser('owner_promo_b@test.local');
|
||
$dept = $this->makeDepartment('PromoDeptB', $owner->userid);
|
||
} catch (\Throwable $e) {
|
||
if ($this->isSwooleInfraFailure($e)) {
|
||
$this->markTestSkipped('Swoole/PushTask 运行时不可用:' . $e->getMessage());
|
||
}
|
||
throw $e;
|
||
}
|
||
|
||
// 模拟"升任后残留":owner 已是部门负责人,但 user_department_owners 仍有他的记录
|
||
\DB::table('user_department_owners')->insertOrIgnore([
|
||
'department_id' => $dept->id,
|
||
'userid' => $owner->userid,
|
||
]);
|
||
$this->assertTrue(
|
||
\DB::table('user_department_owners')
|
||
->where('department_id', $dept->id)
|
||
->where('userid', $owner->userid)
|
||
->exists()
|
||
);
|
||
|
||
// 调用 delDeputy 罢免"当前负责人" → 走防御性早返回路径
|
||
$dept->delDeputy($owner->userid);
|
||
|
||
// 1) user_department_owners 悬挂记录被清理
|
||
$this->assertFalse(
|
||
\DB::table('user_department_owners')
|
||
->where('department_id', $dept->id)
|
||
->where('userid', $owner->userid)
|
||
->exists(),
|
||
'delDeputy(当前负责人) 应清理 user_department_owners 悬挂记录'
|
||
);
|
||
|
||
// 2) 当前负责人仍在 users.department
|
||
$owner = $owner->fresh();
|
||
$this->assertContains($dept->id, $owner->department,
|
||
'当前部门负责人不能被 delDeputy 从 users.department 移除');
|
||
|
||
// 3) 当前负责人仍在部门群(role 不变)
|
||
$dialogUser = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||
->where('userid', $owner->userid)
|
||
->first();
|
||
$this->assertNotNull($dialogUser, '当前部门负责人不能被 delDeputy 移出部门群');
|
||
|
||
// 4) 群 owner_id 仍指向当前负责人
|
||
$dialog = WebSocketDialog::find($dept->dialog_id);
|
||
$this->assertEquals($owner->userid, (int)$dialog->owner_id);
|
||
}
|
||
|
||
/**
|
||
* P0-A:saveDepartment 必须清理新负责人在 user_department_owners 中的残留
|
||
*
|
||
* saveDepartment 内部 joinGroup/pushMsg 依赖 Swoole runtime,
|
||
* 当前 PHPUnit 容器无 Swoole 时该测试将整体异常,
|
||
* 我们捕获到 Swoole 缺失则跳过(业务逻辑不变)
|
||
*/
|
||
public function test_promote_deputy_to_owner_clears_owner_table_record()
|
||
{
|
||
try {
|
||
$owner = $this->makeUser('owner_promo_a@test.local');
|
||
$deputy = $this->makeUser('deputy_promo_a@test.local');
|
||
$dept = $this->makeDepartment('PromoDeptA', $owner->userid);
|
||
|
||
// 直接 DB 写入"管理员"记录,无需 addDeputy
|
||
\DB::table('user_department_owners')->insertOrIgnore([
|
||
'department_id' => $dept->id,
|
||
'userid' => $deputy->userid,
|
||
]);
|
||
// deputy 加入 users.department + 部门群(避免 saveDepartment 路径意外)
|
||
$deputy->department = "," . $dept->id . ",";
|
||
$deputy->save();
|
||
WebSocketDialogUser::updateInsert([
|
||
'dialog_id' => $dept->dialog_id,
|
||
'userid' => $deputy->userid,
|
||
], ['role' => 2]);
|
||
|
||
$dept->saveDepartment([
|
||
'name' => $dept->name,
|
||
'parent_id' => $dept->parent_id,
|
||
'owner_userid' => $deputy->userid,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
if ($this->isSwooleInfraFailure($e)) {
|
||
$this->markTestSkipped('Swoole/PushTask 运行时不可用,saveDepartment 端到端无法验证:' . $e->getMessage());
|
||
}
|
||
throw $e;
|
||
}
|
||
if (!isset($dept)) {
|
||
$this->markTestSkipped('测试环境基础设施失败');
|
||
}
|
||
|
||
$dept = $dept->fresh();
|
||
$this->assertEquals($deputy->userid, (int)$dept->owner_userid);
|
||
$this->assertFalse(
|
||
\DB::table('user_department_owners')
|
||
->where('department_id', $dept->id)
|
||
->where('userid', $deputy->userid)
|
||
->exists(),
|
||
'升任部门负责人后,user_department_owners 中的 deputy 残留必须被清理'
|
||
);
|
||
}
|
||
}
|