package realtime import ( "strings" "testing" ) // ---------- 设备字段的清洗与截断 ---------- func TestSanitizeDeviceText(t *testing.T) { longUA := strings.Repeat("汉", 400) cases := []struct { name string in string limit int want string }{ {"原样保留可读标签", "安卓 13 · Chrome 120 · 1080×2400 @3x · 手机", maxDeviceChars, "安卓 13 · Chrome 120 · 1080×2400 @3x · 手机"}, // 控制字符是能把管理端列表排版搞乱的东西(换行 / 制表 / 退格 / DEL),一律清掉。 {"换行与制表被清掉", " 安卓 13\n\tChrome 120 \r\n", maxDeviceChars, "安卓 13Chrome 120"}, {"DEL 与 C0 控制字符被清掉", "安卓\x00\x1b[31m 13\x7f", maxDeviceChars, "安卓[31m 13"}, {"首尾空白去掉", " 平板 ", maxDeviceChars, "平板"}, {"只空白视作没报", " \t\n ", maxDeviceChars, ""}, {"空串保持空串", "", maxDeviceChars, ""}, // 长度按**字符**不是字节:按字节截会把中文腰斩成半个字。 {"按字符截断(中文)", strings.Repeat("汉", 81), maxDeviceChars, strings.Repeat("汉", 80)}, {"恰好等于上限不截断", strings.Repeat("x", maxDeviceChars), maxDeviceChars, strings.Repeat("x", maxDeviceChars)}, {"超长 UA 截到 300 字符", longUA, maxDeviceRawChars, strings.Repeat("汉", maxDeviceRawChars)}, {"limit <= 0 不截断", strings.Repeat("x", 10), 0, strings.Repeat("x", 10)}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := sanitizeDeviceText(tc.in, tc.limit) if got != tc.want { t.Fatalf("清洗结果不符:\n得到 %q\n期望 %q", got, tc.want) } }) } } // 截断结果必须是合法 UTF-8(半个汉字会显示成替换方块)。 func TestSanitizeDeviceTextKeepsValidUTF8(t *testing.T) { got := sanitizeDeviceText(strings.Repeat("平板", 100), 5) if got != "平板平板平" { t.Fatalf("按字符截断结果应为 %q,得到 %q", "平板平板平", got) } for _, r := range got { if r == '\uFFFD' { t.Fatalf("截断后出现无效 UTF-8 字节: %q", got) } } } // ---------- 同一条连接重复 hello 不许产生新实例 ---------- // 局域网浏览器(hello 不带 instanceId)翻页后 setPage 会再补一条 hello, // 第二次 hello 必须落在**同一个**实例上,否则注册表里会多出一条该设备的离线条目。 func TestRegisterDisplayReusesInstanceForRepeatHelloWithoutID(t *testing.T) { env := newTestEnv(t) env.hub.SetSnapshotMinInterval(0) c := env.dial(t, &helloPayload{Role: RoleDisplay, Page: "standings"}) c.recvEvent(EventSnapshot) first := env.hub.Instances() if len(first) != 1 { t.Fatalf("首次 hello 后应只有一个实例,得到 %d 个: %+v", len(first), first) } if !strings.HasPrefix(first[0].ID, "display-") { t.Fatalf("不带 instanceId 时实例 id 应由服务端生成,得到 %s", first[0].ID) } // 第二次仍不带 instanceId(真实场景:翻页补发的 hello,端上没有本机 id)。 c.send(EventHello, helloPayload{Role: RoleDisplay, Page: "medals"}) c.recvEvent(EventSnapshot) got := env.hub.Instances() if len(got) != 1 { t.Fatalf("重复 hello 后注册表里应仍只有一个实例(旧实现会多出一条离线条目),得到 %d 个: %+v", len(got), got) } if got[0].ID != first[0].ID { t.Fatalf("重复 hello 应复用同一个实例 id:首次 %s,第二次 %s", first[0].ID, got[0].ID) } if !got[0].Online { t.Fatalf("重复 hello 后实例应仍在线: %+v", got[0]) } if got[0].Page != "medals" { t.Fatalf("重复 hello 应刷新当前页为 medals,得到 %s", got[0].Page) } // 引用计数不能虚高:连接断开后实例必须立刻变成离线(虚高会显示成「还挂着一条连接」)。 c.close() waitFor(t, "重复 hello 后实例仍能正确离线", func() bool { insts := env.hub.Instances() return len(insts) == 1 && !insts[0].Online }) } // 换了 instanceId 的连接仍然各自登记:复用只针对「这次没带 id」,不能把显式换 id 吃掉。 func TestRegisterDisplaySwitchesInstanceWhenIDChanges(t *testing.T) { env := newTestEnv(t) env.hub.SetSnapshotMinInterval(0) c := env.dial(t, &helloPayload{Role: RoleDisplay, InstanceID: "screen-1"}) c.recvEvent(EventSnapshot) c.send(EventHello, helloPayload{Role: RoleDisplay, InstanceID: "screen-2"}) c.recvEvent(EventSnapshot) insts := env.hub.Instances() if len(insts) != 2 { t.Fatalf("换 id 后应有两个实例,得到 %d 个: %+v", len(insts), insts) } if !instanceOnline(insts, "screen-2") { t.Fatalf("新 id 应在线: %+v", insts) } // 旧实例的连接引用已归还,只剩「离线」的排查记录。 if instanceOnline(insts, "screen-1") { t.Fatalf("旧 id 的连接引用应已归还,不该仍显示在线: %+v", insts) } } // ---------- 设备字段进注册表 ---------- func TestRegisterDisplayStoresSanitizedDevice(t *testing.T) { env := newTestEnv(t) env.hub.SetSnapshotMinInterval(0) c := env.dial(t, &helloPayload{ Role: RoleDisplay, InstanceID: "pc-1", Device: "安卓 13\n · Chrome 120\t", DeviceRaw: "Mozilla/5.0 (Linux; Android 13)" + strings.Repeat("x", 400), }) c.recvEvent(EventSnapshot) insts := env.hub.Instances() if len(insts) != 1 { t.Fatalf("实例数应为 1,得到 %d", len(insts)) } if insts[0].Device != "安卓 13 · Chrome 120" { t.Fatalf("设备标签应清洗掉换行与制表,得到 %q", insts[0].Device) } if n := len([]rune(insts[0].DeviceRaw)); n != maxDeviceRawChars { t.Fatalf("原始 UA 应按字符截到 %d,得到 %d 个字符", maxDeviceRawChars, n) } if strings.ContainsAny(insts[0].DeviceRaw, "\n\t") { t.Fatalf("原始 UA 不该含控制字符: %q", insts[0].DeviceRaw) } // 老客户端(不带这两个字段)重连:不该把已经报上来的标签抹掉。 c.send(EventHello, helloPayload{Role: RoleDisplay, InstanceID: "pc-1"}) c.recvEvent(EventSnapshot) if got := env.hub.Instances()[0]; got.Device == "" || got.DeviceRaw == "" { t.Fatalf("不带设备字段的重复 hello 不该清空已有标签: %+v", got) } } // 老客户端(不带 device / deviceRaw)行为与今天一致:字段留空,注册表照常登记。 func TestRegisterDisplayWithoutDeviceFields(t *testing.T) { env := newTestEnv(t) c := env.dial(t, &helloPayload{Role: RoleDisplay, InstanceID: "old-1"}) c.recvEvent(EventSnapshot) insts := env.hub.Instances() if len(insts) != 1 || insts[0].ID != "old-1" || !insts[0].Online { t.Fatalf("老客户端应照常登记: %+v", insts) } if insts[0].Device != "" || insts[0].DeviceRaw != "" { t.Fatalf("老客户端没报设备字段时应留空: %+v", insts[0]) } }