- handler 只保留请求绑定、权限检查与响应映射;业务校验顺序、状态码与文案逐字保留 - 新增 service.Error 语义错误,由 handler 统一映射 HTTP 响应 - session 875→361 行、widget 745→321 行、settings 540→73 行 - 自动分配/租户设置(工作时间、欢迎语)/消息校验/黑名单检查/联系方式提取随迁 service 包 - 相关单元测试随迁 service 包,go test ./... 全部通过
185 lines
5.0 KiB
Go
185 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"kefu-cloud/server/internal/model"
|
|
)
|
|
|
|
func TestNormalizeWelcomeSegments(t *testing.T) {
|
|
SetImagePublicBase("http://localhost:9100/kefu")
|
|
|
|
_, _, err := NormalizeWelcomeSegments(nil)
|
|
if err == nil {
|
|
t.Fatal("空列表应失败")
|
|
}
|
|
|
|
ok, first, err := NormalizeWelcomeSegments([]WelcomeSegment{
|
|
{Type: "text", Content: " 你好 "},
|
|
{Type: "image", Content: "http://localhost:9100/kefu/tenants/1/welcome/a.webp"},
|
|
{Type: "text", Content: "请描述问题"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("合法多段应通过: %v", err)
|
|
}
|
|
if len(ok) != 3 || ok[0].Content != "你好" || first != "你好" {
|
|
t.Fatalf("规范化结果异常: %+v first=%s", ok, first)
|
|
}
|
|
|
|
_, _, err = NormalizeWelcomeSegments([]WelcomeSegment{
|
|
{Type: "image", Content: "https://evil.example/x.png"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("非白名单图片 URL 应失败")
|
|
}
|
|
|
|
tooMany := make([]WelcomeSegment, MaxWelcomeSegments+1)
|
|
for i := range tooMany {
|
|
tooMany[i] = WelcomeSegment{Type: "text", Content: "x"}
|
|
}
|
|
if _, _, err := NormalizeWelcomeSegments(tooMany); err == nil {
|
|
t.Fatal("超过段数上限应失败")
|
|
}
|
|
}
|
|
|
|
func TestResolveWelcomeSegmentsCompat(t *testing.T) {
|
|
// 仅有旧字段
|
|
s1 := &model.TenantSetting{WelcomeMessage: "旧欢迎语"}
|
|
segs := ResolveWelcomeSegments(s1)
|
|
if len(segs) != 1 || segs[0].Type != "text" || segs[0].Content != "旧欢迎语" {
|
|
t.Fatalf("旧字段兼容失败: %+v", segs)
|
|
}
|
|
|
|
// 优先 JSON
|
|
s2 := &model.TenantSetting{
|
|
WelcomeMessage: "旧",
|
|
WelcomeMessagesJSON: `[{"type":"text","content":"新1"},{"type":"image","content":"http://x/a.webp"}]`,
|
|
}
|
|
segs = ResolveWelcomeSegments(s2)
|
|
if len(segs) != 2 || segs[0].Content != "新1" {
|
|
t.Fatalf("JSON 优先失败: %+v", segs)
|
|
}
|
|
|
|
// 空 → 默认
|
|
segs = ResolveWelcomeSegments(&model.TenantSetting{})
|
|
if len(segs) != 1 || segs[0].Content != DefaultWelcomeMessage {
|
|
t.Fatalf("默认欢迎语失败: %+v", segs)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeWorkHourSlot(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{"", "", false},
|
|
{" 休息 ", "", false},
|
|
{"全天", "全天", false},
|
|
{"09:00-18:00", "09:00-18:00", false},
|
|
{" 9:30 - 18:05 ", "09:30-18:05", false},
|
|
{"22:00-06:00", "22:00-06:00", false},
|
|
{"全天营业", "", true},
|
|
{"09:00", "", true},
|
|
{"25:00-26:00", "", true},
|
|
{"09:00-18:00-extra", "", true},
|
|
}
|
|
for _, tc := range cases {
|
|
got, err := NormalizeWorkHourSlot(tc.in)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("NormalizeWorkHourSlot(%q) 期望错误,got=%q", tc.in, got)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("NormalizeWorkHourSlot(%q) 意外错误: %v", tc.in, err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("NormalizeWorkHourSlot(%q)=%q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSlotCoversLocal(t *testing.T) {
|
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 2026-03-16 周一
|
|
mk := func(h, m int) time.Time {
|
|
return time.Date(2026, 3, 16, h, m, 0, 0, loc)
|
|
}
|
|
|
|
if !slotCoversLocal("全天", mk(3, 0), loc) {
|
|
t.Fatal("全天应覆盖任意时刻")
|
|
}
|
|
if slotCoversLocal("", mk(10, 0), loc) {
|
|
t.Fatal("空应视为休息")
|
|
}
|
|
if slotCoversLocal("休息", mk(10, 0), loc) {
|
|
t.Fatal("休息应不覆盖")
|
|
}
|
|
// 09:00-18:00 半开区间 [start, end)
|
|
if !slotCoversLocal("09:00-18:00", mk(9, 0), loc) {
|
|
t.Fatal("09:00 应在工作时间内")
|
|
}
|
|
if !slotCoversLocal("09:00-18:00", mk(17, 59), loc) {
|
|
t.Fatal("17:59 应在工作时间内")
|
|
}
|
|
if slotCoversLocal("09:00-18:00", mk(18, 0), loc) {
|
|
t.Fatal("18:00 不应在工作时间内(半开)")
|
|
}
|
|
if slotCoversLocal("09:00-18:00", mk(8, 59), loc) {
|
|
t.Fatal("08:59 不应在工作时间内")
|
|
}
|
|
// 跨午夜 22:00-06:00
|
|
if !slotCoversLocal("22:00-06:00", mk(23, 0), loc) {
|
|
t.Fatal("23:00 应在跨午夜时段内")
|
|
}
|
|
if !slotCoversLocal("22:00-06:00", mk(5, 0), loc) {
|
|
t.Fatal("05:00 应在跨午夜时段内")
|
|
}
|
|
if slotCoversLocal("22:00-06:00", mk(12, 0), loc) {
|
|
t.Fatal("12:00 不应在跨午夜时段内")
|
|
}
|
|
}
|
|
|
|
func TestIsWithinWorkHoursCustomRange(t *testing.T) {
|
|
setting := &model.TenantSetting{
|
|
Timezone: "Asia/Shanghai",
|
|
WorkHoursJSON: `{
|
|
"monday":"09:30-12:00",
|
|
"tuesday":"全天",
|
|
"wednesday":"",
|
|
"thursday":"22:00-06:00",
|
|
"friday":"全天",
|
|
"saturday":"休息",
|
|
"sunday":""
|
|
}`,
|
|
}
|
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
|
|
|
// 周一 10:00 → 在 09:30-12:00
|
|
mon := time.Date(2026, 3, 16, 10, 0, 0, 0, loc)
|
|
if !IsWithinWorkHours(setting, mon) {
|
|
t.Fatal("周一 10:00 应在自定义时段内")
|
|
}
|
|
// 周一 14:00 → 不在
|
|
monPM := time.Date(2026, 3, 16, 14, 0, 0, 0, loc)
|
|
if IsWithinWorkHours(setting, monPM) {
|
|
t.Fatal("周一 14:00 不应在工作时间")
|
|
}
|
|
// 周三休息
|
|
wed := time.Date(2026, 3, 18, 10, 0, 0, 0, loc)
|
|
if IsWithinWorkHours(setting, wed) {
|
|
t.Fatal("周三应休息")
|
|
}
|
|
// 周四跨午夜 23:00
|
|
thu := time.Date(2026, 3, 19, 23, 0, 0, 0, loc)
|
|
if !IsWithinWorkHours(setting, thu) {
|
|
t.Fatal("周四 23:00 应在跨午夜时段")
|
|
}
|
|
}
|