Files
iotlcm/worker-svr/internal/fullsync/contract/compatibility_test.go
T

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
}