- Entry 增加 DeviceVisits:同一 URL 多次/多设备上报取各设备最大值再求和,
不再平方级放大(30 次访问曾被计成 465 次)
- handleGet 不再截断 100 条,返回 {items,total},后台显示当前条数
- 清理死代码(hostVisits/hostOf/visitCount/isCodeHostVal/dashySection)
- 守门测试:重复上报、多设备求和、陈旧数据、持久化、统计、门户文案、第 101 条可见性
136 lines
4.2 KiB
Go
136 lines
4.2 KiB
Go
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次访问")
|
||
}
|
||
}
|
||
}
|
||
}
|