108 lines
4.0 KiB
Go
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
|
|
}
|