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

109 lines
3.5 KiB
Go

package ingest
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"strings"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"rond-with-you/internal/db"
"rond-with-you/internal/rond"
)
// ingestTestDSN 把 ROND_DSN 的库名换成专用库。Import 会把该用户的 is_active 切到自己那份,
// 而 stats 的夹具是「WHERE is_active LIMIT 1」找数据集(不带 user 过滤)——共用库时,
// 本用例运行期间那份临时数据集会被别的包选中,直接串味。换个库谁都不影响谁(库不存在 db.Open 会建)。
func ingestTestDSN(base string) string {
u, err := url.Parse(base)
if err != nil || u.Path == "" {
return base
}
u.Path = strings.TrimSuffix(u.Path, "/") + "_ingest"
return u.String()
}
// 脏时间(parse 阶段被 appleTime 判成 nil 的到访)导入后必须:
// - 不进站点模型(visit_count 不含它);
// - 按「跳过」留档,导出时原样补回(否则一趟导入导出就少一条);
// - 绝不污染 datasets.first_visit_at —— 旧实现会把它回绕成 2001,时间轴直接从 2001 起算。
func TestImportKeepsDirtyTimeOutOfTimeline(t *testing.T) {
base := os.Getenv("ROND_DSN")
if base == "" {
t.Skip("未设置 ROND_DSN")
}
ctx := context.Background()
d, err := db.Open(ingestTestDSN(base))
if err != nil {
t.Fatal(err)
}
// 先注册 close、后注册删用户:Cleanup 是后进先出,删用户时连接还没关。
t.Cleanup(func() { d.Close() })
if err := db.Migrate(ctx, d); err != nil {
t.Fatal(err)
}
var userID int64
uname := fmt.Sprintf("__dirty_%d", time.Now().UnixNano())
if err := d.QueryRowContext(ctx,
`INSERT INTO users (username, password_hash) VALUES ($1,'x') RETURNING id`, uname).Scan(&userID); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { d.Exec(`DELETE FROM users WHERE id=$1`, userID) })
good := time.Date(2026, 9, 20, 8, 0, 0, 0, time.UTC)
b := &rond.Backup{
VisitUUID: map[int]string{1: strings.Repeat("a", 32)},
ActivityUUID: map[int]string{},
Locations: []rond.Location{{SrcPK: 1, Name: "家", Lat: 24.1, Lon: 109.1}},
Visits: []rond.Visit{
{SrcPK: 1, Arrival: &good, LocationSrcPK: intp(1)},
{SrcPK: 2, Arrival: nil, LocationSrcPK: intp(1)}, // 脏时间被 appleTime 判成 nil
},
RawRows: map[string]map[int]map[string]any{
"ZVISIT": {2: {"Z_PK": int64(2), "ZARRIVALDATE_": 1e30}},
},
}
datasetID, st, err := Import(ctx, d, userID, "dirty.rondbackup", strings.Repeat("0", 64), 1, b)
if err != nil {
t.Fatal(err)
}
if st.Visits != 1 {
t.Errorf("只该导入 1 条到访(脏时间那条跳过),实际 %d", st.Visits)
}
var stored int
if err := d.QueryRowContext(ctx,
`SELECT count(*) FROM visits WHERE dataset_id=$1`, datasetID).Scan(&stored); err != nil {
t.Fatal(err)
}
if stored != 1 {
t.Errorf("库里到访应为 1 条,实际 %d", stored)
}
var archived int
if err := d.QueryRowContext(ctx,
`SELECT count(*) FROM entity_raw WHERE dataset_id=$1 AND entity='ZVISIT' AND skipped`, datasetID).Scan(&archived); err != nil {
t.Fatal(err)
}
if archived != 1 {
t.Errorf("脏时间的到访应当留档为 skipped,实际 %d 条", archived)
}
var first, last sql.NullTime
if err := d.QueryRowContext(ctx,
`SELECT first_visit_at, last_visit_at FROM datasets WHERE id=$1`, datasetID).Scan(&first, &last); err != nil {
t.Fatal(err)
}
if !first.Valid || !first.Time.Equal(good) {
t.Errorf("first_visit_at 应等于合法到访的时间 %s,实际 %v", good.Format(time.RFC3339), first)
}
}