Files
iotlcm/internal/fullsync/contract/compatibility_test.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。

- Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件)
- 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、
  库名 xiujie_admin → iotlcm、哨兵名占位
- 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异,
  不破坏「校验 MSISDN 格式」等测试断言)
- 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像
  digest 记录
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23),
  移除写死的 git 凭据
- README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0
  LICENSE,LF 行尾 + .gitattributes

已知:3 个测试包在源仓同一提交上同样失败(client/upstream、
service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

108 lines
4.0 KiB
Go

package contract_test
import (
"encoding/json"
"reflect"
"testing"
"time"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/fullsync/contract"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset"
)
// Check every old field, including zero/omitempty behavior, so future adapters
// can adopt the contract without silently changing the persisted wire format.
func TestLegacyFieldSchemas(t *testing.T) {
for _, pair := range []struct{ old, next any }{
{zset.RunMeta{}, contract.RunMeta{}},
{zset.FailureRecord{}, contract.FailureRecord{}},
{zset.Task{}, contract.Task{}},
} {
old, next := reflect.TypeOf(pair.old), reflect.TypeOf(pair.next)
for i := 0; i < old.NumField(); i++ {
field := old.Field(i)
replacement, ok := next.FieldByName(field.Name)
if !ok || replacement.Tag.Get("json") != field.Tag.Get("json") || replacement.Type.Kind() != field.Type.Kind() {
t.Errorf("%s.%s lost its legacy schema: old=%+v new=%+v", old.Name(), field.Name, field, replacement)
}
}
}
}
func TestLegacyRunJSONRoundTrip(t *testing.T) {
now := time.Date(2026, 9, 20, 1, 2, 3, 456, time.UTC)
legacy := zset.RunMeta{
RunID: "legacy", Operation: "current_month_usage_full_sync", QueryMonth: "202609",
Status: "completed_with_failures", ProducerDone: true, CurrentRound: 5,
SuccessCount: 9, FailedCount: 1, CreatedAt: now, ProducerCompletedAt: now, CompletedAt: now,
}
data := mustJSON(t, legacy)
var decoded contract.RunMeta
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatal(err)
}
if err := decoded.Validate(); err != nil {
t.Fatalf("legacy run rejected: %v", err)
}
if got := string(mustJSON(t, decoded)); got != string(data) {
t.Fatalf("legacy wire format changed:\n%s\n%s", data, got)
}
// New fields do not prevent old readers from consuming the same metadata.
decoded.SyncType = contract.SyncReplay
decoded.SourceRunID = "source"
decoded.ReplayCycle = 1
decoded.EnqueuedCount = 10
decoded.PendingFailureCount = 1
var oldReader zset.RunMeta
if err := json.Unmarshal(mustJSON(t, decoded), &oldReader); err != nil {
t.Fatal(err)
}
oldReader.SyncType = ""
oldReader.SourceRunID = ""
oldReader.ReplayCycle = 0
oldReader.EnqueuedCount = 0
oldReader.PendingFailureCount = 0
if !reflect.DeepEqual(oldReader, legacy) {
t.Fatalf("old reader lost fields: %+v", oldReader)
}
}
func TestLegacyFailureAndTaskJSON(t *testing.T) {
legacy := zset.FailureRecord{
Version: 1, TaskID: "source:op:42", OriginalRunID: "source", LatestRunID: "latest",
QueryMonth: "202609", Operation: "op", CardID: 42, ICCID: "iccid", MSISDN: "msisdn", IMSI: "imsi",
ChanKey: "chan", TempKey: "template", Carrier: "mobile", FailureType: "mobile_busy_exhausted",
Round: 5, Attempts: 7, Code: 13000, ErrCode: "BUSY", Message: "try again",
ReviewStatus: "pending", ReviewRemark: "remark", ReviewedBy: "operator", ReviewedAt: "2026-09-20",
RetryCycle: 2, ReplayRunID: "replayed", FirstFailedAt: time.Unix(1, 0).UTC(), LastFailedAt: time.Unix(2, 0).UTC(),
}
var record contract.FailureRecord
if err := json.Unmarshal(mustJSON(t, legacy), &record); err != nil {
t.Fatal(err)
}
if string(mustJSON(t, record)) != string(mustJSON(t, legacy)) {
t.Fatal("failure record wire format changed")
}
if summary := record.Summary(); summary.ErrorCategory != contract.ErrorRateLimited || summary.TaskID != legacy.TaskID || summary.FailedAt != legacy.LastFailedAt {
t.Fatalf("incorrect failure projection: %+v", summary)
}
task := zset.Task{TaskID: "r:op:42", RunID: "r", Operation: "op", QueryMonth: "202609", ChanKey: "c", TempKey: "t", Operator: 2, CardID: 42, ICCID: "i", MSISDN: "m", IMSI: "s", NextExecuteAt: 1234}
var newTask contract.Task
if err := json.Unmarshal(mustJSON(t, task), &newTask); err != nil {
t.Fatal(err)
}
newTask.NextExecuteAt = 9999
if string(mustJSON(t, newTask)) != string(mustJSON(t, task)) {
t.Fatal("task wire format or scheduling exclusion changed")
}
}
func mustJSON(t *testing.T, value any) []byte {
t.Helper()
data, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return data
}