Files
tamakyi b4c2a8452f 审计修复:后端(安全 / 数据一致性 / 性能)
audit-2026-09-21.md 里的后端条目落地。每条都配了回归守卫,并逐条做过变异验证
(注入原 bug → 确认按正确理由变红 → 还原 → 变绿)。

安全
- CSRF 只加同源 Origin/Referer 守卫(csrf.go),不动模板
- 会话 cookie 的 Secure 按请求判断;两步验证两闸(loginGuard + 验证码一次性)
- 跳转收口到 safeInternalPath;clientIP 只认 XFF 最后一段
- GPX 按属主圈定且查 RowsAffected;下载名走 downloadDisposition
- 上传口统一 MaxBytesReader;落盘后的失败路径清掉半成品

数据一致性
- 区域判定两端各过一遍再 AND(apiTrack 与 movementsWhere 对齐)
- Filter.Verdict 全域一致(下推到 visitsWhere);聚合刷新必须返回 error
- 附属数据改挂坐标键,Go 与 SQL 两套表达式逐位一致
- 脏备份的伪时间/越界坐标不再进领域模型;rows 循环统一 drainRows
- 合并与导出:writeRawRow 上抛错误、悬空引用清理、原生列补全

性能
- placeCols 相关子查询带 dataset_id 前导列;findSourceArchive 先按大小筛
- 批量勾选改 ANY($3::bigint[]);fillupCache 加上限;同一查询不再查两遍

另:新增「全新库」冒烟测试 —— 空库跑完 Migrate 再用应用侧读查询真打一遍,
迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
2026-09-24 20:37:16 +08:00

204 lines
7.7 KiB
Go

package rond
import (
"database/sql"
"math"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func nullF(v float64) sql.NullFloat64 { return sql.NullFloat64{Float64: v, Valid: true} }
// 脏值必须判成「装不下」(nil)。旧实现直接 int64(sec),超范围是回绕不是饱和:
// 实测 NaN/±Inf/1e30 恰好回绕到基准点(2001-01-01)、1e12 落到 2123 年。
func TestAppleTimeRejectsDirtyValues(t *testing.T) {
dirty := map[string]float64{
"NaN": math.NaN(),
"+Inf": math.Inf(1),
"-Inf": math.Inf(-1),
"1e30": 1e30,
"-1e30": -1e30,
"1e12": 1e12, // 会落到 2123 年
"-1e12": -1e12,
"早于窗口": appleMinSec - 86400,
"晚于窗口": appleMaxSec + 86400,
}
for name, v := range dirty {
if tm := appleTime(nullF(v)); tm != nil {
t.Errorf("%s 应当被拒(nil),实际得到 %s", name, tm.Format(time.RFC3339Nano))
}
}
if appleTime(sql.NullFloat64{}) != nil {
t.Error("NULL 应当返回 nil")
}
}
// 合法值要原样保留,尤其是微秒精度——整数/小数分开算就是为了它。
func TestAppleTimeKeepsLegitValues(t *testing.T) {
for _, want := range []time.Time{
time.Date(1995, 7, 1, 12, 30, 45, 123456000, time.UTC),
time.Date(2018, 3, 9, 21, 4, 5, 987654000, time.UTC),
time.Date(2026, 9, 21, 10, 19, 9, 1000, time.UTC),
} {
got := appleTime(nullF(want.Sub(appleEpoch).Seconds()))
if got == nil {
t.Fatalf("%s 被误拒", want.Format(time.RFC3339Nano))
}
if !got.Equal(want) {
t.Errorf("时间失真: 期望 %s 得到 %s", want.Format(time.RFC3339Nano), got.Format(time.RFC3339Nano))
}
}
for _, edge := range []float64{appleMinSec, appleMaxSec} {
if appleTime(nullF(edge)) == nil {
t.Errorf("窗口边界 %.0f 不该被拒", edge)
}
}
}
func TestValidCoord(t *testing.T) {
for _, c := range [][2]float64{
{math.NaN(), 0}, {0, math.NaN()}, {math.Inf(1), 0}, {0, math.Inf(-1)},
{91, 0}, {-91, 0}, {0, 181}, {0, -181}, {999, 999},
} {
if validCoord(c[0], c[1]) {
t.Errorf("无效坐标 (%v,%v) 被判成可用", c[0], c[1])
}
}
for _, c := range [][2]float64{{0, 0}, {24.1, 109.1}, {90, 180}, {-90, -180}, {39.9042, 116.4074}} {
if !validCoord(c[0], c[1]) {
t.Errorf("合法坐标 (%v,%v) 被误拒", c[0], c[1])
}
}
}
// 函数再对,没接上去也白搭:读 ZLOCATION 的循环必须过一遍 validCoord。
func TestParseSanitizesLocationCoords(t *testing.T) {
_, self, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("拿不到测试文件路径")
}
src, err := os.ReadFile(filepath.Join(filepath.Dir(self), "parse.go"))
if err != nil {
t.Fatal(err)
}
text := string(src)
i := strings.Index(text, "FROM ZLOCATION")
if i < 0 {
t.Fatal("parse.go 里找不到 ZLOCATION 查询")
}
j := strings.Index(text[i:], "b.Locations = append")
if j < 0 {
t.Fatal("找不到了 ZLOCATION 循环的收尾")
}
if !strings.Contains(text[i:i+j], "validCoord(") {
t.Error("读 ZLOCATION 的循环没有调用 validCoord 归一化坐标")
}
}
// 端到端:脏备份走一遍真实的 Parse,不许把伪时间/越界坐标带进领域模型。
func TestParseRejectsDirtyTimeAndCoords(t *testing.T) {
good := time.Date(2026, 9, 20, 8, 0, 0, 0, time.UTC)
goodSec := good.Sub(appleEpoch).Seconds()
path := filepath.Join(t.TempDir(), "Backup.sqlite")
sdb, err := sql.Open("sqlite", "file:"+filepath.ToSlash(path))
if err != nil {
t.Fatal(err)
}
defer sdb.Close()
for _, stmt := range []string{
`CREATE TABLE ZTAGGROUP (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT)`,
`CREATE TABLE ZTAG (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCOLOR_ TEXT, ZGROUP INTEGER)`,
`CREATE TABLE ZACTIVITY (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCOLOR_ TEXT, ZICON_ TEXT,
ZISHOME INTEGER, ZISWORK INTEGER, ZISEXCLUDED INTEGER, ZISARCHIVED INTEGER, ZUID_ BLOB)`,
`CREATE TABLE ZTRANSPORT (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCOLOR_ TEXT, ZICON_ TEXT)`,
`CREATE TABLE ZLOCATION (Z_PK INTEGER PRIMARY KEY, ZNAME_ TEXT, ZCATEGORY_ TEXT,
ZLATITUDE REAL, ZLONGITUDE REAL, ZISOCOUNTRYCODE TEXT, ZADMINISTRATIVEAREA TEXT,
ZLOCALITY TEXT, ZSUBLOCALITY TEXT, ZTHOROUGHFARE TEXT, ZSUBTHOROUGHFARE TEXT, ZTIMEZONE TEXT)`,
`CREATE TABLE ZVISIT (Z_PK INTEGER PRIMARY KEY, ZLOCATION INTEGER, ZACTIVITY_ INTEGER,
ZARRIVALDATE_ REAL, ZDEPARTUREDATE_ REAL, ZBOOKMARKED INTEGER, ZUSERADDED INTEGER,
ZREMARK_ TEXT, ZEMOJINAME_ TEXT, ZWEATHERSYMBOL_ TEXT, ZIDENTIFIER_ BLOB)`,
`CREATE TABLE Z_11VISITS_ (Z_18VISITS_ INTEGER, Z_11TAGS_5 INTEGER)`,
`CREATE TABLE ZMOVEMENT (Z_PK INTEGER PRIMARY KEY, ZTRANSPORT_ INTEGER, ZVISITFROM_ INTEGER,
ZVISITTO_ INTEGER, ZSTART_ REAL, ZEND_ REAL)`,
`CREATE TABLE ZHOURLYWEATHER (Z_PK INTEGER PRIMARY KEY, ZVISIT INTEGER, ZDATE_ REAL,
ZISDAYLIGHT INTEGER, ZTEMPERATURE_ REAL, ZAPPARENTTEMPERATURE_ REAL, ZHUMIDITY_ REAL,
ZPRECIPITATIONAMOUNT_ REAL, ZPRECIPITATIONCHANCE_ REAL, ZWINDSPEED_ REAL, ZVISIBILITY_ REAL,
ZUVINDEXVALUE_ INTEGER, ZCONDITION_ TEXT, ZSYMBOLNAME_ TEXT, ZUVINDEXCATEGORY_ TEXT,
ZWINDCOMPASSDIRECTION_ TEXT)`,
} {
if _, err := sdb.Exec(stmt); err != nil {
t.Fatalf("建表失败: %v", err)
}
}
exec := func(q string, args ...any) {
t.Helper()
if _, err := sdb.Exec(q, args...); err != nil {
t.Fatalf("插入失败: %v", err)
}
}
exec(`INSERT INTO ZACTIVITY (Z_PK, ZNAME_, ZCOLOR_, ZICON_, ZISHOME, ZISWORK, ZISEXCLUDED, ZISARCHIVED)
VALUES (1,'通勤','#fff','🚇',0,0,0,0)`)
// 1=正常;2=坐标越界;3=坐标量级离谱(模拟 NaN 入库前的形态)
exec(`INSERT INTO ZLOCATION (Z_PK, ZNAME_, ZLATITUDE, ZLONGITUDE) VALUES (1,'正常',24.1,109.1)`)
exec(`INSERT INTO ZLOCATION (Z_PK, ZNAME_, ZLATITUDE, ZLONGITUDE) VALUES (2,'越界',999,-999)`)
exec(`INSERT INTO ZLOCATION (Z_PK, ZNAME_, ZLATITUDE, ZLONGITUDE) VALUES (3,'离谱',1e30,0)`)
// 1=正常时间;2=ZARRIVALDATE_ 落到 2123 年;3=量级离谱(旧实现回绕成 2001)
exec(`INSERT INTO ZVISIT (Z_PK, ZLOCATION, ZACTIVITY_, ZARRIVALDATE_, ZDEPARTUREDATE_)
VALUES (1,1,1,?,?)`, goodSec, goodSec+3600)
exec(`INSERT INTO ZVISIT (Z_PK, ZLOCATION, ZACTIVITY_, ZARRIVALDATE_) VALUES (2,2,1,1e12)`)
exec(`INSERT INTO ZVISIT (Z_PK, ZLOCATION, ZACTIVITY_, ZARRIVALDATE_) VALUES (3,3,1,-1e30)`)
// 位移:时间坏了保留「有行、无时间」,不能写成伪时间
exec(`INSERT INTO ZMOVEMENT (Z_PK, ZTRANSPORT_, ZVISITFROM_, ZVISITTO_, ZSTART_, ZEND_)
VALUES (1,1,1,1,?,?)`, goodSec, goodSec+600)
exec(`INSERT INTO ZMOVEMENT (Z_PK, ZTRANSPORT_, ZVISITFROM_, ZVISITTO_, ZSTART_, ZEND_)
VALUES (2,1,1,1,1e12,1e12)`)
b, err := Parse(sdb)
if err != nil {
t.Fatal(err)
}
byLoc := map[int]Location{}
for _, l := range b.Locations {
byLoc[l.SrcPK] = l
}
if got := byLoc[1]; got.Lat != 24.1 || got.Lon != 109.1 {
t.Errorf("正常坐标被改动了: (%v,%v)", got.Lat, got.Lon)
}
if got := byLoc[2]; got.Lat != 0 || got.Lon != 0 {
t.Errorf("越界坐标应归零,实际 (%v,%v)", got.Lat, got.Lon)
}
if got := byLoc[3]; got.Lat != 0 || got.Lon != 0 {
t.Errorf("量级离谱的坐标应归零,实际 (%v,%v)", got.Lat, got.Lon)
}
byVisit := map[int]Visit{}
for _, v := range b.Visits {
byVisit[v.SrcPK] = v
}
if v := byVisit[1]; v.Arrival == nil || !v.Arrival.Equal(good) {
t.Errorf("正常到访时间丢了: %v", v.Arrival)
}
for _, pk := range []int{2, 3} {
if v := byVisit[pk]; v.Arrival != nil {
t.Errorf("到访 #%d 的脏时间应当被拒,实际 %s", pk, v.Arrival.Format(time.RFC3339))
}
}
if len(b.Movements) != 2 {
t.Fatalf("位移条数不对: %d", len(b.Movements))
}
if m := b.Movements[0]; m.Start == nil || !m.Start.Equal(good) {
t.Errorf("正常位移时间丢了: %v", m.Start)
}
if m := b.Movements[1]; m.Start != nil || m.End != nil {
t.Errorf("脏位移时间应当为空而不是伪时间: start=%v end=%v", m.Start, m.End)
}
}