Files
HomeDash/history-server/countgate_test.go
JianFeeeee 292e77097d fix(history): 访问计数按设备去重 + 后台返回全量
- Entry 增加 DeviceVisits:同一 URL 多次/多设备上报取各设备最大值再求和,
  不再平方级放大(30 次访问曾被计成 465 次)
- handleGet 不再截断 100 条,返回 {items,total},后台显示当前条数
- 清理死代码(hostVisits/hostOf/visitCount/isCodeHostVal/dashySection)
- 守门测试:重复上报、多设备求和、陈旧数据、持久化、统计、门户文案、第 101 条可见性
2026-10-02 11:24:55 +08:00

136 lines
4.2 KiB
Go
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.

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"gopkg.in/yaml.v3"
)
// 本文件是一组**守门测试**:把 2026-09-13 定位到的那个真实缺陷
// (visit_count 绝对值被当增量累加)钉死,防止以后再改回去。
// 守门 1:绝不出现平方级放大。
func TestNoQuadraticInflation(t *testing.T) {
withTempHistory(t)
// 真实访问 20 次;扩展每 30s 同步一次,30 秒内反复上报绝对值
for round := 1; round <= 20; round++ {
postSync(t, SyncItem{
URL: "https://ex.com/gate", Device: "laptop",
Timestamp: float64(1700000000 + round*30), VisitCount: round,
})
}
if got := history["https://ex.com/gate"].TotalVisits; got != 20 {
t.Fatalf("TotalVisits = %d,真实只访问 20 次;出现放大即回归", got)
}
}
// 守门 2:多设备各自的绝对值要相加(这是"取 max"的错误修法会失败的用例)。
func TestMultiDeviceStillSums(t *testing.T) {
withTempHistory(t)
postSync(t, SyncItem{URL: "https://ex.com/m", Device: "laptop", Timestamp: 1700000000, VisitCount: 8})
postSync(t, SyncItem{URL: "https://ex.com/m", Device: "phone", Timestamp: 1700000030, VisitCount: 3})
postSync(t, SyncItem{URL: "https://ex.com/m", Device: "tablet", Timestamp: 1700000060, VisitCount: 5})
if got := history["https://ex.com/m"].TotalVisits; got != 16 {
t.Errorf("TotalVisits = %d;期望 8+3+5=16(全局 max 会错成 8)", got)
}
}
// 守门 3:落盘后能读回,且计数不变(DeviceVisits 参与序列化)。
func TestCountsSurviveReload(t *testing.T) {
withTempHistory(t)
postSync(t, SyncItem{URL: "https://ex.com/r", Device: "laptop", Timestamp: 1700000000, VisitCount: 6})
postSync(t, SyncItem{URL: "https://ex.com/r", Device: "phone", Timestamp: 1700000030, VisitCount: 2})
data, err := os.ReadFile(filepath.Join(dataDir, "history.json"))
if err != nil {
t.Fatalf("未落盘: %v", err)
}
var entries []*Entry
if err := json.Unmarshal(data, &entries); err != nil {
t.Fatalf("history.json 解析失败: %v", err)
}
if len(entries) != 1 || entries[0].TotalVisits != 8 {
t.Fatalf("重载后 TotalVisits = %v,期望 8", entries)
}
if len(entries[0].DeviceVisits) != 2 {
t.Errorf("device_visits 未落盘: %v", entries[0].DeviceVisits)
}
}
// 守门 4:handleStats 的 total_visits 同样不得放大。
func TestStatsNotInflated(t *testing.T) {
withTempHistory(t)
for round := 1; round <= 10; round++ {
postSync(t, SyncItem{URL: "https://ex.com/s", Device: "laptop",
Timestamp: float64(1700000000 + round*30), VisitCount: round})
}
req := httptest.NewRequest(http.MethodGet, "/stats", nil)
w := httptest.NewRecorder()
handleStats(w, req)
var out struct {
Unique int `json:"unique_urls"`
Total int `json:"total_visits"`
}
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
t.Fatalf("stats 解析失败: %v", err)
}
if out.Total != 10 {
t.Errorf("total_visits = %d;真实 10 次访问,期望 10", out.Total)
}
}
// 守门 5:rebuildDashySections 产出的「N次访问」文案要反映真实计数。
func TestHistoryDescriptionUsesRealCount(t *testing.T) {
dir := t.TempDir()
oldDir := dataDir
dataDir = dir
defer func() { dataDir = oldDir }()
// conf.yml 需含「常用服务」与「历史访问」两个分组
conf := `sections:
- name: 常用服务
items: []
- name: 历史访问
items: []
`
if err := os.WriteFile(filepath.Join(dir, "conf.yml"), []byte(conf), 0o644); err != nil {
t.Fatal(err)
}
history = map[string]*Entry{
"https://ex.com/x": {URL: "https://ex.com/x", Title: "X", TotalVisits: 9},
}
rebuildDashySections()
data, err := os.ReadFile(filepath.Join(dir, "conf.yml"))
if err != nil {
t.Fatal(err)
}
var cfg struct {
Sections []struct {
Name string `yaml:"name"`
Items []struct {
Title string `yaml:"title"`
Description string `yaml:"description"`
} `yaml:"items"`
} `yaml:"sections"`
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
t.Fatal(err)
}
for _, s := range cfg.Sections {
if s.Name != histSectionZh {
continue
}
for _, it := range s.Items {
if it.Title == "X" && it.Description != "9次访问" {
t.Errorf("历史项文案 = %q,期望 %q", it.Description, "9次访问")
}
}
}
}