task(), // 测试环境无 Swoole 运行时;绑定一个最小 stub 让 Task::deliver 安全降级,仅验证 DB 状态。 // OnlineData::live 也会读取 swoole 的 onlineDataTable,提供一个 fake table(get 始终返回 0)。 if (!app()->bound('swoole')) { $fakeTable = new class { public function get($key) { return 0; } public function set($key, $value) { return true; } public function del($key) { return true; } public function exist($key) { return false; } public function incr($key, $col, $incrBy = 1) { return 1; } public function decr($key, $col, $decrBy = 1) { return 0; } }; app()->instance('swoole', new class($fakeTable) { public $worker_id = 0; public $taskworker = false; public $setting = ['worker_num' => 1]; public $onlineDataTable; public $globalDataTable; public function __construct($fakeTable) { $this->onlineDataTable = $fakeTable; $this->globalDataTable = $fakeTable; } public function task($task) { return false; } public function sendMessage($task, $workerId) { return false; } }); } } 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(int $ownerUserid, string $name = null): UserDepartment { $name = $name ?? 'Dept_' . substr(md5(uniqid('', true)), 0, 6); $dept = UserDepartment::createInstance(); $dept->saveDepartment([ 'name' => $name, 'parent_id' => 0, 'owner_userid' => $ownerUserid, ]); return $dept->fresh(); } /** * 模拟 department__adddeputy(绕过 User::auth('admin') / HTTP)。 * 直接调用 UserDepartment::addDeputy,确保 simulate 与真实接口一致。 * 注:addDeputy 方法在 Task 5 才实现;本 simulate 在 Task 5 之前调用会报错。 */ private function simulateAddDeputy(UserDepartment $dept, int $userid): void { $dept->addDeputy($userid); } private function simulateDelDeputy(UserDepartment $dept, int $userid): void { $dept->delDeputy($userid); } public function test_setup_works() { $owner = $this->makeUser('d1_owner@test.local'); $dept = $this->makeDepartment($owner->userid); $this->assertEquals($owner->userid, $dept->owner_userid); $this->assertNotEmpty($dept->dialog_id); } public function test_helpers_and_deputy_userids_accessor() { $owner = $this->makeUser('d3_o@test.local'); $deputy = $this->makeUser('d3_d@test.local'); $member = $this->makeUser('d3_m@test.local'); $dept = $this->makeDepartment($owner->userid); // 手动插入副记录(addDeputy 在 Task 5 才实现) DB::table('user_department_owners')->insert([ 'department_id' => $dept->id, 'userid' => $deputy->userid, ]); $dept = $dept->fresh(); $this->assertTrue($dept->isPrimaryOwner($owner->userid)); $this->assertFalse($dept->isPrimaryOwner($deputy->userid)); $this->assertFalse($dept->isPrimaryOwner($member->userid)); $this->assertFalse($dept->isDeputyOwner($owner->userid)); $this->assertTrue($dept->isDeputyOwner($deputy->userid)); $this->assertFalse($dept->isDeputyOwner($member->userid)); $this->assertTrue($dept->isOwner($owner->userid)); $this->assertTrue($dept->isOwner($deputy->userid)); $this->assertFalse($dept->isOwner($member->userid)); $deputyIds = $dept->deputy_userids; $this->assertEquals([$deputy->userid], $deputyIds); // 序列化后 API 响应应包含 deputy_userids $arr = $dept->toArray(); $this->assertArrayHasKey('deputy_userids', $arr); $this->assertEquals([$deputy->userid], $arr['deputy_userids']); } public function test_saveDepartment_owner_change_syncs_dialog_role() { $oldOwner = $this->makeUser('d4_old@test.local'); $newOwner = $this->makeUser('d4_new@test.local'); $dept = $this->makeDepartment($oldOwner->userid); // 手动加 newOwner 入群(saveDepartment 之前他不在群里) // joinGroup($userid, $inviter, $important=null, $pushMsg=true) — pushMsg=false 跳过 Swoole $dialog = WebSocketDialog::find($dept->dialog_id); $dialog->joinGroup($newOwner->userid, 0, null, false); // 转让主负责人 $dept->saveDepartment([ 'name' => $dept->name, 'parent_id' => $dept->parent_id, 'owner_userid' => $newOwner->userid, ]); $oldRole = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) ->where('userid', $oldOwner->userid)->value('role'); $newRole = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) ->where('userid', $newOwner->userid)->value('role'); $this->assertEquals(0, (int)$oldRole, '原主应降为普通成员'); $this->assertEquals(1, (int)$newRole, '新主 role 应为 1'); } public function test_saveDepartment_owner_change_preserves_deputies() { $oldOwner = $this->makeUser('d4b_old@test.local'); $newOwner = $this->makeUser('d4b_new@test.local'); $deputy = $this->makeUser('d4b_dep@test.local'); $dept = $this->makeDepartment($oldOwner->userid); // 加 deputy 入群 + 副记录 + role=2(pushMsg=false 跳过 Swoole) $dialog = WebSocketDialog::find($dept->dialog_id); $dialog->joinGroup($deputy->userid, 0, null, false); DB::table('user_department_owners')->insert([ 'department_id' => $dept->id, 'userid' => $deputy->userid, ]); WebSocketDialogUser::where('dialog_id', $dialog->id) ->where('userid', $deputy->userid)->update(['role' => 2]); // 加 newOwner 入群 $dialog->joinGroup($newOwner->userid, 0, null, false); // 转让 $dept->saveDepartment([ 'name' => $dept->name, 'parent_id' => $dept->parent_id, 'owner_userid' => $newOwner->userid, ]); // 副表保留 $this->assertContains($deputy->userid, $dept->fresh()->deputy_userids); // 副 role 保留 $depRole = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) ->where('userid', $deputy->userid)->value('role'); $this->assertEquals(2, (int)$depRole); } public function test_addDeputy_creates_owner_record_and_joins_group_as_deputy() { $owner = $this->makeUser('d5_o@test.local'); $deputy = $this->makeUser('d5_d@test.local'); $dept = $this->makeDepartment($owner->userid); $this->simulateAddDeputy($dept, $deputy->userid); $dept = $dept->fresh(); $this->assertContains($deputy->userid, $dept->deputy_userids); // 副已入群 $exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) ->where('userid', $deputy->userid)->exists(); $this->assertTrue($exists); // 副 role=2 $role = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) ->where('userid', $deputy->userid)->value('role'); $this->assertEquals(2, (int)$role); } public function test_addDeputy_idempotent() { $owner = $this->makeUser('d5b_o@test.local'); $deputy = $this->makeUser('d5b_d@test.local'); $dept = $this->makeDepartment($owner->userid); $this->simulateAddDeputy($dept, $deputy->userid); $this->simulateAddDeputy($dept, $deputy->userid); $count = DB::table('user_department_owners') ->where('department_id', $dept->id) ->where('userid', $deputy->userid)->count(); $this->assertEquals(1, $count); } public function test_addDeputy_rejects_primary_owner() { $owner = $this->makeUser('d5c_o@test.local'); $dept = $this->makeDepartment($owner->userid); $this->expectException(\App\Exceptions\ApiException::class); $this->simulateAddDeputy($dept, $owner->userid); } public function test_addDeputy_rejects_nonexistent_user() { $owner = $this->makeUser('d5d_o@test.local'); $dept = $this->makeDepartment($owner->userid); $this->expectException(\App\Exceptions\ApiException::class); $this->simulateAddDeputy($dept, 99999); } public function test_delDeputy_removes_owner_record_and_exits_department_group() { $owner = $this->makeUser('d6_o@test.local'); $deputy = $this->makeUser('d6_d@test.local'); $dept = $this->makeDepartment($owner->userid); $this->simulateAddDeputy($dept, $deputy->userid); // 任命后副应该入 users.department 并加入部门群 $this->assertContains($dept->id, User::find($deputy->userid)->department, '任命副后应加入 users.department'); $this->assertTrue( WebSocketDialogUser::where('dialog_id', $dept->dialog_id)->where('userid', $deputy->userid)->exists(), '任命副后应加入部门群' ); $this->simulateDelDeputy($dept, $deputy->userid); $dept = $dept->fresh(); $this->assertNotContains($deputy->userid, $dept->deputy_userids); // 罢免后从 users.department 移除(与主"离开部门"对齐) $this->assertNotContains($dept->id, User::find($deputy->userid)->department, '罢免副后应从 users.department 移除'); // 退出部门群(成员关系=群关系一致) $exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) ->where('userid', $deputy->userid)->exists(); $this->assertFalse($exists, '罢免副后应退出部门群(成员关系=群关系)'); } public function test_delDeputy_idempotent_for_non_deputy() { $owner = $this->makeUser('d6b_o@test.local'); $member = $this->makeUser('d6b_m@test.local'); $dept = $this->makeDepartment($owner->userid); // member 不是副,调 delDeputy 不应抛错 $this->simulateDelDeputy($dept, $member->userid); $this->assertTrue(true); } public function test_deleteDepartment_cleans_deputy_records() { $owner = $this->makeUser('d7_o@test.local'); $deputy = $this->makeUser('d7_d@test.local'); $dept = $this->makeDepartment($owner->userid); $this->simulateAddDeputy($dept, $deputy->userid); $deptId = $dept->id; $dept->deleteDepartment(); $count = DB::table('user_department_owners')->where('department_id', $deptId)->count(); $this->assertEquals(0, $count); } public function test_user_transfer_clears_departing_deputy_role() { $owner = $this->makeUser('d7b_o@test.local'); $departing = $this->makeUser('d7b_dep@test.local'); $receiver = $this->makeUser('d7b_rec@test.local'); $dept = $this->makeDepartment($owner->userid); $this->simulateAddDeputy($dept, $departing->userid); UserDepartment::transfer($departing->userid, $receiver->userid); // 离职的副记录已删 $this->assertNotContains($departing->userid, $dept->fresh()->deputy_userids); // receiver 没有继承副身份 $this->assertNotContains($receiver->userid, $dept->fresh()->deputy_userids); } public function test_user_transfer_inherits_departing_primary() { // 主转让仍要把主权位传给接收人(保留现有行为) $departing = $this->makeUser('d7c_dep@test.local'); $receiver = $this->makeUser('d7c_rec@test.local'); $dept = $this->makeDepartment($departing->userid); UserDepartment::transfer($departing->userid, $receiver->userid); $this->assertEquals($receiver->userid, $dept->fresh()->owner_userid); } public function test_deleteDepartment_recursively_cleans_child_deputies() { // 父部门 + 子部门各有副,删父部门时副记录应级联清理 $owner = $this->makeUser('d7d_o@test.local'); $deputyParent = $this->makeUser('d7d_dp@test.local'); $deputyChild = $this->makeUser('d7d_dc@test.local'); $parent = $this->makeDepartment($owner->userid, 'ParentDept'); $this->simulateAddDeputy($parent, $deputyParent->userid); $child = UserDepartment::createInstance(); $child->saveDepartment([ 'name' => 'ChildDept', 'parent_id' => $parent->id, 'owner_userid' => $owner->userid, ]); $child = $child->fresh(); $this->simulateAddDeputy($child, $deputyChild->userid); $parentId = $parent->id; $childId = $child->id; $parent->deleteDepartment(); // 递归删子部门 + 清各自副 $this->assertEquals(0, DB::table('user_department_owners')->where('department_id', $parentId)->count()); $this->assertEquals(0, DB::table('user_department_owners')->where('department_id', $childId)->count()); } }