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 再用应用侧读查询真打一遍, 迁移块排到依赖表之前这种错只有新库会炸,老库毫无症状。
204 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|