package ingest import ( "fmt" "testing" "time" "rond-with-you/internal/rond" ) // sampleBackup 造一份最小的备份:到访 + 地点 + 活动,编号与 UUID 分别给定。 func sampleBackup(pks []int, uuids []string) *rond.Backup { b := &rond.Backup{VisitUUID: map[int]string{}, ActivityUUID: map[int]string{}} for i, pk := range pks { at := time.Unix(int64(1700000000+i*3600), 0) b.Visits = append(b.Visits, rond.Visit{SrcPK: pk, Arrival: &at, LocationSrcPK: intp(1)}) b.VisitUUID[pk] = uuids[i] } b.Locations = []rond.Location{{SrcPK: 1, Name: "家", Lat: 24.1, Lon: 109.1}} b.Activities = []rond.Activity{{SrcPK: 9, Name: "工作"}} b.ActivityUUID[9] = "cccccccccccccccccccccccccccccccc" return b } func intp(v int) *int { return &v } // 同一批数据重新导出一次:UUID 一个没变、Z_PK 全被打乱(顺序也变了), // 指纹必须一样。否则会被判成「新内容」→ 新建数据集 → 按 dataset_id 隔离的 // 备注与投票全部失联。 func TestContentHashIgnoresPKReorder(t *testing.T) { a := sampleBackup([]int{1, 2, 3}, []string{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "dddddddddddddddddddddddddddddddd", }) b := sampleBackup([]int{555, 276, 88}, []string{ "dddddddddddddddddddddddddddddddd", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", }) if contentHash(a) != contentHash(b) { t.Fatal("编号重排不该改变指纹") } } // 内容真的变了(多一条到访)就得算不同的指纹,否则新数据会被当成重复上传而丢掉。 func TestContentHashDetectsNewVisit(t *testing.T) { a := sampleBackup([]int{1, 2}, []string{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", }) b := sampleBackup([]int{1, 2, 3}, []string{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "dddddddddddddddddddddddddddddddd", }) if contentHash(a) == contentHash(b) { t.Fatal("多了一条到访就是不同内容") } } // 只改了地点名(到访一条没动)也算内容变了:站点是靠地名认地点的。 func TestContentHashDetectsPlaceRename(t *testing.T) { a := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) b := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) b.Locations[0].Name = "老家" if contentHash(a) == contentHash(b) { t.Fatal("地点改名应改变指纹") } } // 没有 UUID 的老备份退回「到达时间 + 地点坐标」,同一批数据重导仍要认出是同一份。 func TestContentHashFallsBackWithoutUUID(t *testing.T) { a := sampleBackup([]int{1, 2}, []string{"", ""}) b := sampleBackup([]int{700, 900}, []string{"", ""}) if contentHash(a) != contentHash(b) { t.Fatal("没有 UUID 时应按到达时间与地点兜底,不该受编号影响") } c := sampleBackup([]int{1, 2}, []string{"", ""}) c.Visits[1].Arrival = ptrTime(time.Unix(1700000000, 0)) // 改掉第二条的时间 if contentHash(a) == contentHash(c) { t.Fatal("时间不同应算不同内容") } } // 没有到达时间的行导入时就被跳过,不该参与指纹。 func TestContentHashSkipsRowsWithoutArrival(t *testing.T) { a := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) b := sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) b.Visits = append(b.Visits, rond.Visit{SrcPK: 42}) if contentHash(a) != contentHash(b) { t.Fatal("缺到达时间的行不该影响指纹") } } // 指纹是稳定的十六进制摘要,不是随手拼的字符串。 func TestContentHashShape(t *testing.T) { h := contentHash(sampleBackup([]int{1}, []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})) if len(h) != 64 { t.Fatalf("应是 64 位 sha256 十六进制,实际 %d 位: %s", len(h), h) } if _, err := fmt.Sscanf(h, "%x", new(string)); err != nil { t.Fatalf("不是纯十六进制: %v", err) } } func ptrTime(t time.Time) *time.Time { return &t }