mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-08 03:50:50 +08:00
411 lines
17 KiB
PHP
411 lines
17 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace tests\Feature;
|
||
|
||
use app\common\service\NoticeService;
|
||
use app\common\test\TestCase;
|
||
use think\facade\Cache;
|
||
use think\facade\Db;
|
||
|
||
/**
|
||
* NoticeService 核心行为集成测试.
|
||
*
|
||
* 覆盖 11 个契约点(与计划任务 3 逐项对齐,每项一个 test 方法):
|
||
* 1. send 主体 + target 展开
|
||
* 2. 'all' 广播(仅 status=1,含无 email 管理员)
|
||
* 3. channels 缺省按 scene.channels 展开
|
||
* 4. 订阅过滤(退订行拦截投递)
|
||
* 5. locked 场景强投递
|
||
* 6. locked 场景请求未注册渠道被 ∩ 掉(零 target)
|
||
* 7. 未注册 scene 放行(缺省 [inmail])
|
||
* 8. 无 email 跳过 email 渠道
|
||
* 9. inmail 全局总闸关闭跳过(键不存在视为开)
|
||
* 10. unreadCount/markRead/markAllRead 数字正确
|
||
* 11. markRead 越权防护
|
||
*
|
||
* fixture 均为事务内自造数据,tearDown 自动回滚;缓存清理显式执行
|
||
* (Cache 不受事务回滚影响,见 setInmailSwitch/clearInmailSwitch)。
|
||
*/
|
||
class NoticeServiceTest extends TestCase
|
||
{
|
||
// =========================================================================
|
||
// 1. send 主体 + target 展开
|
||
// =========================================================================
|
||
|
||
public function test_send_creates_notice_and_expands_inmail_target(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
|
||
$noticeId = NoticeService::send([$adminId], '测试标题', '测试内容');
|
||
|
||
$this->assertGreaterThan(0, $noticeId);
|
||
$this->assertDatabaseHas('system_notice', [
|
||
'id' => $noticeId,
|
||
'scene' => 'common',
|
||
'title' => '测试标题',
|
||
'content' => '测试内容',
|
||
'biz_id' => '',
|
||
'send_admin_id' => 0,
|
||
]);
|
||
|
||
$target = Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)
|
||
->where('admin_id', $adminId)
|
||
->find();
|
||
$this->assertNotNull($target, 'target 行应存在');
|
||
$this->assertSame('inmail', $target['channel']);
|
||
$this->assertSame(2, (int) $target['status'], 'inmail 投递即落库,直接成功');
|
||
$this->assertSame('', $target['target_addr'], 'inmail 无需外部地址');
|
||
$this->assertGreaterThan(0, (int) $target['send_time']);
|
||
$this->assertSame(1, Db::name('system_notice_target')->where('notice_id', $noticeId)->count());
|
||
}
|
||
|
||
// =========================================================================
|
||
// 2. 'all' 广播
|
||
// =========================================================================
|
||
|
||
public function test_send_all_broadcasts_to_active_admins_only(): void
|
||
{
|
||
// 记录广播前的启用管理员基数(测试库含初始数据,不能假设从 0 开始)
|
||
$activeBefore = Db::name('system_admin')->where('status', 1)->count();
|
||
$withMailBefore = Db::name('system_admin')->where('status', 1)->where('email', '<>', '')->count();
|
||
|
||
$adminWithMail = $this->createAdmin(['email' => 'all_a@test.com']);
|
||
$adminNoMail = $this->createAdmin();
|
||
$adminDisabled = $this->createAdmin(['status' => 0]);
|
||
|
||
$noticeId = NoticeService::send('all', '广播标题', '广播内容');
|
||
|
||
$this->assertGreaterThan(0, $noticeId);
|
||
|
||
// 总展开数 = 全体启用管理员各 1 行 inmail + 有 email 的管理员各 1 行 email
|
||
$expectedTotal = ($activeBefore + 2) + ($withMailBefore + 1);
|
||
$actualTotal = Db::name('system_notice_target')->where('notice_id', $noticeId)->count();
|
||
$this->assertSame($expectedTotal, $actualTotal, "target 总数应为 {$expectedTotal},实际 {$actualTotal}");
|
||
|
||
// 禁用管理员不在广播内
|
||
$this->assertSame(
|
||
0,
|
||
Db::name('system_notice_target')->where('notice_id', $noticeId)->where('admin_id', $adminDisabled)->count(),
|
||
'status=0 的管理员不应收到广播'
|
||
);
|
||
|
||
// 无 email 管理员仅 inmail 一行
|
||
$this->assertSame(1, Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->count());
|
||
$this->assertSame(1, Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->where('channel', 'inmail')->count());
|
||
|
||
// 有 email 管理员 inmail + email 两行,email 行待发
|
||
$this->assertSame(2, Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminWithMail)->count());
|
||
$emailRow = Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminWithMail)->where('channel', 'email')->find();
|
||
$this->assertNotNull($emailRow);
|
||
$this->assertSame(0, (int) $emailRow['status'], 'email 渠道应置待发由消费者投递');
|
||
$this->assertSame('all_a@test.com', $emailRow['target_addr']);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 3. channels 缺省按 scene.channels 展开
|
||
// =========================================================================
|
||
|
||
public function test_send_without_channels_expands_scene_channels(): void
|
||
{
|
||
$adminNoMail = $this->createAdmin();
|
||
$adminWithMail = $this->createAdmin(['email' => 'scene@test.com']);
|
||
|
||
// admin_login 注册渠道 [inmail,email],不传 channels 按场景展开
|
||
$noticeId = NoticeService::send([$adminNoMail, $adminWithMail], '登录提醒', '内容', ['scene' => 'admin_login']);
|
||
|
||
// 无 email 管理员:email 被跳过,仅 inmail 一行
|
||
$this->assertSame(1, Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->count());
|
||
$this->assertSame(1, Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminNoMail)->where('channel', 'inmail')->count());
|
||
|
||
// 有 email 管理员:两行,email 行地址正确
|
||
$this->assertSame(2, Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)->where('admin_id', $adminWithMail)->count());
|
||
$this->assertDatabaseHas('system_notice_target', [
|
||
'notice_id' => $noticeId,
|
||
'admin_id' => $adminWithMail,
|
||
'channel' => 'email',
|
||
'target_addr' => 'scene@test.com',
|
||
'status' => 0,
|
||
]);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 4. 订阅过滤
|
||
// =========================================================================
|
||
|
||
public function test_unsubscribe_row_filters_channel_delivery(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
|
||
// 默认订阅:第一次 send 产生 inmail 行
|
||
$noticeId1 = NoticeService::send([$adminId], '第一次', '内容');
|
||
$this->assertSame(1, Db::name('system_notice_target')->where('notice_id', $noticeId1)->count());
|
||
|
||
// 显式退订 common 场景的 inmail
|
||
$this->insertSubscribe($adminId, 'common', 'inmail', 0);
|
||
|
||
$noticeId2 = NoticeService::send([$adminId], '第二次', '内容');
|
||
// notice 主体仍存在,但零 target(email 渠道因无地址跳过,inmail 被退订过滤)
|
||
$this->assertDatabaseHas('system_notice', ['id' => $noticeId2]);
|
||
$this->assertSame(
|
||
0,
|
||
Db::name('system_notice_target')->where('notice_id', $noticeId2)->count(),
|
||
'全部退订 common 后不应产生任何 target 行'
|
||
);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 5. locked 强投递
|
||
// =========================================================================
|
||
|
||
public function test_locked_scene_forces_delivery_despite_unsubscribe(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
$this->insertSubscribe($adminId, 'system', 'inmail', 0);
|
||
|
||
$noticeId = NoticeService::send([$adminId], '系统公告', '内容', ['scene' => 'system']);
|
||
|
||
$target = Db::name('system_notice_target')
|
||
->where('notice_id', $noticeId)
|
||
->where('admin_id', $adminId)
|
||
->find();
|
||
$this->assertNotNull($target, 'locked 场景即使退订也强制投递');
|
||
$this->assertSame('inmail', $target['channel']);
|
||
$this->assertSame(2, (int) $target['status']);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 6. locked 场景请求未注册渠道被 ∩ 掉
|
||
// =========================================================================
|
||
|
||
public function test_locked_scene_intersects_requested_channels(): void
|
||
{
|
||
$adminId = $this->createAdmin(['email' => 'locked@test.com']);
|
||
|
||
// system 场景注册渠道仅 [inmail];locked 不豁免 ∩,请求 email 应被裁掉 → 零 target
|
||
$noticeId = NoticeService::send([$adminId], '系统公告', '内容', ['scene' => 'system', 'channels' => ['email']]);
|
||
|
||
$this->assertDatabaseHas('system_notice', ['id' => $noticeId]);
|
||
$this->assertSame(
|
||
0,
|
||
Db::name('system_notice_target')->where('notice_id', $noticeId)->count(),
|
||
'locked 场景请求未注册渠道应被 ∩ 掉,零 target'
|
||
);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 7. 未注册 scene 放行
|
||
// =========================================================================
|
||
|
||
public function test_unregistered_scene_passes_with_default_inmail(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
|
||
// 未注册场景:Log::warning 后放行,缺省渠道 [inmail]。
|
||
// warning 断言取舍:框架 Log::listen 实为 LogWrite 事件监听,
|
||
// 仅在日志通道 save 时触发(请求收尾),CLI 测试进程不触发,
|
||
// 监听式断言不可行 —— 此处断言行为(放行 + 缺省 inmail 展开)。
|
||
$noticeId = NoticeService::send([$adminId], '业务自定义', '内容', ['scene' => 'totally_new_scene']);
|
||
|
||
$this->assertDatabaseHas('system_notice', ['id' => $noticeId, 'scene' => 'totally_new_scene']);
|
||
$this->assertSame(1, Db::name('system_notice_target')->where('notice_id', $noticeId)->count());
|
||
$this->assertDatabaseHas('system_notice_target', [
|
||
'notice_id' => $noticeId,
|
||
'channel' => 'inmail',
|
||
'status' => 2,
|
||
]);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 8. 无 email 跳过 email 渠道
|
||
// =========================================================================
|
||
|
||
public function test_missing_email_skips_email_channel(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
|
||
// 显式请求 email 渠道但管理员无 email 值:跳过该渠道,不写行
|
||
$noticeId = NoticeService::send([$adminId], '标题', '内容', ['scene' => 'common', 'channels' => ['email']]);
|
||
|
||
$this->assertDatabaseHas('system_notice', ['id' => $noticeId]);
|
||
$this->assertSame(
|
||
0,
|
||
Db::name('system_notice_target')->where('notice_id', $noticeId)->count(),
|
||
'无 email 值的管理员不应产生 email target 行'
|
||
);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 9. inmail 全局总闸
|
||
// =========================================================================
|
||
|
||
public function test_inmail_global_switch_off_skips_inmail(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
|
||
// 插入关闭配置行(value='0')并清 sysconfig 缓存(缓存不受事务回滚影响)
|
||
$this->setInmailSwitch('0');
|
||
|
||
$noticeId1 = NoticeService::send([$adminId], '总闸关闭', '内容');
|
||
$this->assertDatabaseHas('system_notice', ['id' => $noticeId1]);
|
||
$this->assertSame(
|
||
0,
|
||
Db::name('system_notice_target')->where('notice_id', $noticeId1)->count(),
|
||
'notify_inmail_enable=0 时 inmail 渠道应全局跳过'
|
||
);
|
||
|
||
// 移除配置行:键不存在视为开(全局总闸默认放行)
|
||
$this->clearInmailSwitch();
|
||
|
||
$noticeId2 = NoticeService::send([$adminId], '总闸恢复', '内容');
|
||
$this->assertDatabaseHas('system_notice_target', [
|
||
'notice_id' => $noticeId2,
|
||
'channel' => 'inmail',
|
||
'status' => 2,
|
||
]);
|
||
}
|
||
|
||
// =========================================================================
|
||
// 10. unreadCount / markRead / markAllRead
|
||
// =========================================================================
|
||
|
||
public function test_unread_count_and_read_actions(): void
|
||
{
|
||
$adminId = $this->createAdmin();
|
||
|
||
NoticeService::send([$adminId], '第一条', '内容');
|
||
NoticeService::send([$adminId], '第二条', '内容');
|
||
NoticeService::send([$adminId], '第三条', '内容');
|
||
$this->assertSame(3, NoticeService::unreadCount($adminId));
|
||
|
||
$targetIds = Db::name('system_notice_target')
|
||
->where('admin_id', $adminId)
|
||
->where('read_time', 0)
|
||
->column('id');
|
||
|
||
// 标记两条已读
|
||
$marked = NoticeService::markRead([$targetIds[0], $targetIds[1]], $adminId);
|
||
$this->assertSame(2, $marked);
|
||
$this->assertSame(1, NoticeService::unreadCount($adminId));
|
||
$this->assertGreaterThan(0, (int) Db::name('system_notice_target')->where('id', $targetIds[0])->value('read_time'));
|
||
|
||
// 重复标记不再增加
|
||
$this->assertSame(0, NoticeService::markRead([$targetIds[0]], $adminId), '已读行不应被重复标记');
|
||
|
||
// 全部已读
|
||
$this->assertSame(1, NoticeService::markAllRead($adminId));
|
||
$this->assertSame(0, NoticeService::unreadCount($adminId));
|
||
}
|
||
|
||
// =========================================================================
|
||
// 11. markRead 越权防护
|
||
// =========================================================================
|
||
|
||
public function test_mark_read_cannot_touch_other_admins_rows(): void
|
||
{
|
||
$adminA = $this->createAdmin();
|
||
$adminB = $this->createAdmin();
|
||
|
||
NoticeService::send([$adminA], 'A的通知', '内容');
|
||
NoticeService::send([$adminB], 'B的通知', '内容');
|
||
$this->assertSame(1, NoticeService::unreadCount($adminA));
|
||
$this->assertSame(1, NoticeService::unreadCount($adminB));
|
||
|
||
$bTargetId = (int) Db::name('system_notice_target')
|
||
->where('admin_id', $adminB)
|
||
->value('id');
|
||
|
||
// adminA 尝试读 adminB 的行:影响 0 行,adminB 数据不受影响
|
||
$this->assertSame(0, NoticeService::markRead([$bTargetId], $adminA), 'markRead 不得触碰他人行');
|
||
$this->assertSame(0, (int) Db::name('system_notice_target')->where('id', $bTargetId)->value('read_time'));
|
||
$this->assertSame(1, NoticeService::unreadCount($adminB));
|
||
|
||
// markAllRead 同样带 admin_id 条件:只影响自己
|
||
NoticeService::markAllRead($adminA);
|
||
$this->assertSame(0, NoticeService::unreadCount($adminA));
|
||
$this->assertSame(1, NoticeService::unreadCount($adminB), 'markAllRead 不得触碰他人行');
|
||
}
|
||
|
||
// =========================================================================
|
||
// fixture 工厂(事务内运行,tearDown 自动回滚)
|
||
// =========================================================================
|
||
|
||
/**
|
||
* 造启用管理员.
|
||
*
|
||
* @param array $overrides 字段覆盖(email/status 等)
|
||
* @return int 管理员ID
|
||
*/
|
||
private function createAdmin(array $overrides = []): int
|
||
{
|
||
$data = array_merge([
|
||
'username' => 'ntc_' . uniqid(),
|
||
'password' => 'not-a-real-password',
|
||
'nickname' => '通知测试用户',
|
||
'email' => '',
|
||
'status' => 1,
|
||
'create_time' => time(),
|
||
'update_time' => time(),
|
||
'delete_time' => 0,
|
||
], $overrides);
|
||
|
||
return (int) Db::name('system_admin')->insertGetId($data);
|
||
}
|
||
|
||
/**
|
||
* 插入订阅矩阵行.
|
||
*/
|
||
private function insertSubscribe(int $adminId, string $scene, string $channel, int $status): void
|
||
{
|
||
Db::name('system_notice_subscribe')->insert([
|
||
'admin_id' => $adminId,
|
||
'scene' => $scene,
|
||
'channel' => $channel,
|
||
'status' => $status,
|
||
'create_time' => time(),
|
||
'update_time' => time(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 写 notify 组 inmail 总闸配置行并清缓存.
|
||
*
|
||
* sysconfig 缓存不受事务回滚影响:非 '0' 值会缓存 3600 秒,
|
||
* 必须显式清 tag 才能让后续读取看到事务内的新值。
|
||
*/
|
||
private function setInmailSwitch(string $value): void
|
||
{
|
||
Db::name('system_config')->insert([
|
||
'name' => 'notify_inmail_enable',
|
||
'group' => 'notify',
|
||
'value' => $value,
|
||
'remark' => 'NoticeServiceTest fixture',
|
||
'sort' => 100,
|
||
'create_time' => time(),
|
||
'update_time' => time(),
|
||
]);
|
||
Cache::tag('sysconfig')->clear();
|
||
}
|
||
|
||
/**
|
||
* 删除 notify 组 inmail 总闸配置行并清缓存.
|
||
*/
|
||
private function clearInmailSwitch(): void
|
||
{
|
||
Db::name('system_config')
|
||
->where('group', 'notify')
|
||
->where('name', 'notify_inmail_enable')
|
||
->delete();
|
||
Cache::tag('sysconfig')->clear();
|
||
}
|
||
}
|