dootask/tests/Feature/TodoSetPermissionTest.php
kuaifan aa2e0acaba feat(dialog): 系统设置支持禁止其他人员设置/取消聊天待办
新增系统级开关 todo_set_permission(open=允许默认 / close=禁止)。
开关为禁止时,仅本人、群主/群管理员、项目负责人/项目管理员、任务负责人
可设置或取消聊天消息待办,其他人由后端拦截;默认允许,保持现有行为。

- SystemController::setting 接入开关读写(白名单 + 默认 open)
- WebSocketDialog::checkTodoOwnerPermission 角色判断(复用 isOwner 等)
- WebSocketDialogMsg::toggleTodoMsg 内权限闸门:close 且影响到他人且
  非放行角色时 retError;仅影响自己始终放行;open 时行为零变化
- SystemSetting.vue「消息相关」新增「待办设置权限」开关 UI
- 国际化文案(original-api.txt / original-web.txt)
- TodoSetPermissionTest 覆盖角色判断、闸门决策及真实拦截路径(8 用例)

任务 #124。系统后台 admin 不特殊放行;「完成待办」不在本次范围。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 15:12:46 +00:00

243 lines
10 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\Feature;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use App\Models\ProjectUser;
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgTodo;
use App\Models\WebSocketDialogUser;
use App\Module\Base;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
/**
* 待办设置/取消权限测试(系统开关 todo_set_permission
*/
class TodoSetPermissionTest extends TestCase
{
use DatabaseTransactions;
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;
}
/** 普通群group_type=user设置群主与群管理员 */
private function makeUserGroup(int $ownerUserid, array $members, array $deputyUserids = []): WebSocketDialog
{
$all = array_values(array_unique(array_merge([$ownerUserid], $members)));
$dialog = WebSocketDialog::createGroup('Test_group', $all, 'user', $ownerUserid);
if ($deputyUserids) {
WebSocketDialogUser::where('dialog_id', $dialog->id)
->whereIn('userid', $deputyUserids)
->update(['role' => 2]);
}
return $dialog->fresh();
}
/** 项目 + 项目群group_type=projectowner=负责人deputy=项目管理员 */
private function makeProjectWithDialog(int $ownerUserid, array $members = [], array $deputyUserids = []): Project
{
$all = array_values(array_unique(array_merge([$ownerUserid], $members)));
$project = Project::createInstance([
'name' => 'Test_' . substr(md5(uniqid('', true)), 0, 6),
'desc' => '',
'userid' => $ownerUserid,
'personal' => 0,
]);
$project->save();
ProjectUser::updateInsert(['project_id' => $project->id, 'userid' => $ownerUserid], ['owner' => 1]);
foreach ($all as $uid) {
if ($uid === $ownerUserid) continue;
ProjectUser::updateInsert(['project_id' => $project->id, 'userid' => $uid], ['owner' => 0]);
}
if ($deputyUserids) {
ProjectUser::where('project_id', $project->id)
->whereIn('userid', $deputyUserids)
->update(['owner' => 2]);
}
$dialog = WebSocketDialog::createGroup('Test_pdialog', $all, 'project', $ownerUserid);
$project->dialog_id = $dialog->id;
$project->save();
$project->syncDialogUser();
return $project->fresh();
}
public function test_group_owner_and_deputy_allowed_others_not()
{
$owner = $this->makeUser('t_g_o@test.local');
$deputy = $this->makeUser('t_g_d@test.local');
$member = $this->makeUser('t_g_m@test.local');
$dialog = $this->makeUserGroup($owner->userid, [$deputy->userid, $member->userid], [$deputy->userid]);
$this->assertTrue($dialog->checkTodoOwnerPermission($owner->userid));
$this->assertTrue($dialog->checkTodoOwnerPermission($deputy->userid));
$this->assertFalse($dialog->checkTodoOwnerPermission($member->userid));
$this->assertFalse($dialog->checkTodoOwnerPermission(0));
}
public function test_project_owner_and_deputy_allowed_member_not()
{
$owner = $this->makeUser('t_p_o@test.local');
$deputy = $this->makeUser('t_p_d@test.local');
$member = $this->makeUser('t_p_m@test.local');
$project = $this->makeProjectWithDialog($owner->userid, [$deputy->userid, $member->userid], [$deputy->userid]);
$dialog = WebSocketDialog::find($project->dialog_id);
$this->assertTrue($dialog->checkTodoOwnerPermission($owner->userid));
$this->assertTrue($dialog->checkTodoOwnerPermission($deputy->userid));
$this->assertFalse($dialog->checkTodoOwnerPermission($member->userid));
}
public function test_task_owner_and_task_project_owner_allowed_member_not()
{
$projectOwner = $this->makeUser('t_t_po@test.local');
$taskOwner = $this->makeUser('t_t_to@test.local');
$member = $this->makeUser('t_t_m@test.local');
$project = $this->makeProjectWithDialog($projectOwner->userid, [$taskOwner->userid, $member->userid]);
// 任务群group_type=task
$taskDialog = WebSocketDialog::createGroup(
'Test_tdialog',
[$projectOwner->userid, $taskOwner->userid, $member->userid],
'task',
$projectOwner->userid
);
$task = ProjectTask::createInstance([
'project_id' => $project->id,
'parent_id' => 0,
'name' => 'Test task',
'dialog_id' => $taskDialog->id,
'userid' => $projectOwner->userid,
]);
$task->save();
ProjectTaskUser::createInstance([
'project_id' => $project->id,
'task_id' => $task->id,
'userid' => $taskOwner->userid,
'owner' => 1,
])->save();
$taskDialog = $taskDialog->fresh();
$this->assertTrue($taskDialog->checkTodoOwnerPermission($taskOwner->userid), '任务负责人应放行');
$this->assertTrue($taskDialog->checkTodoOwnerPermission($projectOwner->userid), '任务所属项目负责人应放行');
$this->assertFalse($taskDialog->checkTodoOwnerPermission($member->userid), '普通成员应拒绝');
}
/**
* 镜像 WebSocketDialogMsg::toggleTodoMsg 内的权限闸门决策。
* @param string $switch 开关值 open|close
* @param int $sender 操作者
* @param array $cancel 本次取消待办的用户
* @param array $setup 本次新增待办的用户
* @return bool true=放行
*/
private function gateAllow(WebSocketDialog $dialog, string $switch, int $sender, array $cancel, array $setup): bool
{
if ($switch !== 'close') {
return true; // 开关非 close行为不变全部放行
}
$affected = array_unique(array_merge($cancel, $setup));
$others = array_diff($affected, [$sender]);
if (!$others) {
return true; // 仅影响自己 → 放行
}
return $dialog->checkTodoOwnerPermission($sender);
}
public function test_gate_open_switch_allows_anyone()
{
$owner = $this->makeUser('t_gate_o@test.local');
$member = $this->makeUser('t_gate_m@test.local');
$dialog = $this->makeUserGroup($owner->userid, [$member->userid]);
// 普通成员给他人设待办,开关 open → 放行
$this->assertTrue($this->gateAllow($dialog, 'open', $member->userid, [], [$owner->userid, $member->userid]));
}
public function test_gate_close_self_only_allowed()
{
$owner = $this->makeUser('t_gate2_o@test.local');
$member = $this->makeUser('t_gate2_m@test.local');
$dialog = $this->makeUserGroup($owner->userid, [$member->userid]);
// 普通成员仅给/取消自己的待办 → 放行
$this->assertTrue($this->gateAllow($dialog, 'close', $member->userid, [], [$member->userid]));
$this->assertTrue($this->gateAllow($dialog, 'close', $member->userid, [$member->userid], []));
}
public function test_gate_close_member_to_others_blocked()
{
$owner = $this->makeUser('t_gate3_o@test.local');
$member = $this->makeUser('t_gate3_m@test.local');
$dialog = $this->makeUserGroup($owner->userid, [$member->userid]);
// 普通成员给他人设待办,开关 close → 拒绝
$this->assertFalse($this->gateAllow($dialog, 'close', $member->userid, [], [$owner->userid]));
// 普通成员取消他人待办 → 拒绝
$this->assertFalse($this->gateAllow($dialog, 'close', $member->userid, [$owner->userid], []));
}
public function test_gate_close_owner_to_others_allowed()
{
$owner = $this->makeUser('t_gate4_o@test.local');
$member = $this->makeUser('t_gate4_m@test.local');
$dialog = $this->makeUserGroup($owner->userid, [$member->userid]);
// 群主给他人设待办,开关 close → 放行
$this->assertTrue($this->gateAllow($dialog, 'close', $owner->userid, [], [$member->userid]));
}
/**
* 真实调用 toggleTodoMsg覆盖闸门「被拦截路径」
* 开关 close 下,普通成员给他人设待办应被提前 retError 拦截,
* 不会执行后续的 sendMsg无 WebSocket 副作用)。
*/
public function test_toggle_todo_msg_blocks_member_to_others_when_close()
{
$owner = $this->makeUser('t_toggle_o@test.local');
$member = $this->makeUser('t_toggle_m@test.local');
$dialog = $this->makeUserGroup($owner->userid, [$member->userid]);
// 系统开关设为 close合并写入避免清空其它 system 设置)
$setting = Base::setting('system', ['todo_set_permission' => 'close'], true);
$this->assertSame('close', $setting['todo_set_permission']);
$this->assertSame('close', Base::settingFind('system', 'todo_set_permission'));
// 群主发一条真实文本消息type 必须是 text否则被 in_array 提前拒绝)
$msg = WebSocketDialogMsg::createInstance([
'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type,
'userid' => $owner->userid,
'type' => 'text',
'msg' => ['text' => 'hello todo'],
'read' => 0,
'send' => 1,
]);
$msg->save();
// 普通成员给群主(他人)设待办 → 被闸门拦截
$res = $msg->toggleTodoMsg($member->userid, [$owner->userid]);
$this->assertTrue(Base::isError($res), '被拦截路径应返回错误响应');
$this->assertSame(0, $res['ret']);
$this->assertStringContainsString('仅群主、项目/任务负责人可设置或取消他人待办', $res['msg']);
// 被拦截后不应写入任何待办记录
$this->assertSame(0, WebSocketDialogMsgTodo::whereMsgId($msg->id)->count());
}
}