package contract_test import ( "encoding/json" "errors" "fmt" "math" "strings" "testing" "unicode/utf8" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/fullsync/contract" ) func TestBoundedErrorCategories(t *testing.T) { for raw, want := range map[string]contract.ErrorCategory{ "timeout": contract.ErrorTimeout, " RATE_LIMITED ": contract.ErrorRateLimited, "authentication": contract.ErrorAuthentication, "validation": contract.ErrorValidation, "upstream": contract.ErrorUpstream, "transport": contract.ErrorTransport, "internal": contract.ErrorInternal, "other": contract.ErrorOther, "network": contract.ErrorTransport, "mobile_busy": contract.ErrorRateLimited, "mobile_busy_exhausted": contract.ErrorRateLimited, "channel_blocked": contract.ErrorRateLimited, "channel_limited": contract.ErrorRateLimited, "business": contract.ErrorUpstream, "carrier_retry": contract.ErrorUpstream, "canceled": contract.ErrorInternal, "": contract.ErrorOther, "card-898600-unique-error": contract.ErrorOther, } { if got := contract.NormalizeErrorCategory(raw); got != want { t.Errorf("normalize %q = %q, want %q", raw, got, want) } var decoded contract.ErrorCategory if err := json.Unmarshal(mustJSON(t, raw), &decoded); err != nil || decoded != want { t.Errorf("decode %q = %q, err %v", raw, decoded, err) } if got := string(mustJSON(t, contract.ErrorCategory(raw))); got != string(mustJSON(t, string(want))) { t.Errorf("encode %q = %s", raw, got) } } } func TestErrorCountsCannotGrowWithCardinality(t *testing.T) { counts := contract.ErrorCounts{contract.ErrorTimeout: 2, contract.ErrorOther: 3, "network": 4} for i := 0; i < 1000; i++ { counts[contract.ErrorCategory(fmt.Sprintf("card-error-%d", i))] = 1 } var decoded contract.ErrorCounts if err := json.Unmarshal(mustJSON(t, counts), &decoded); err != nil { t.Fatal(err) } if len(decoded) != 3 || len(decoded) > contract.MaxErrorCategories || decoded[contract.ErrorOther] != 1003 || decoded[contract.ErrorTransport] != 4 { t.Fatalf("unbounded or lost counts: %+v", decoded) } if err := json.Unmarshal([]byte(`{"unknown":2,"different":3,"other":4,"network":5}`), &decoded); err != nil { t.Fatal(err) } if len(decoded) != 2 || decoded[contract.ErrorOther] != 9 || decoded[contract.ErrorTransport] != 5 { t.Fatalf("input normalization failed: %+v", decoded) } } func TestErrorCountsRejectInvalidNumbers(t *testing.T) { for _, raw := range []string{`{"timeout":-1}`, `{"one":9223372036854775807,"two":1}`, `{"other":1.5}`, `{"other":"1"}`} { var counts contract.ErrorCounts if err := json.Unmarshal([]byte(raw), &counts); !errors.Is(err, contract.ErrInvalidArgument) { t.Errorf("invalid input %s returned %v", raw, err) } } for _, counts := range []contract.ErrorCounts{{"timeout": -1}, {"one": math.MaxInt64, "two": 1}} { if _, err := json.Marshal(counts); !errors.Is(err, contract.ErrInvalidArgument) { t.Errorf("invalid output counts returned %v", err) } } } func TestBoundedFailureMessage(t *testing.T) { for _, message := range []string{"short", strings.Repeat("x", 3000), strings.Repeat("卡", 1000), strings.Repeat("x", 2047) + "卡", "invalid\xfftext"} { got := contract.BoundedErrorMessage(message) if len(got) > contract.MaxErrorMessageBytes || !utf8.ValidString(got) { t.Fatalf("invalid bounded output: bytes=%d", len(got)) } if utf8.ValidString(message) && len(message) <= contract.MaxErrorMessageBytes && got != message { t.Fatal("short message changed") } } record := contract.FailureRecord{TaskID: "task", Message: strings.Repeat("卡", 1000)} if err := record.Summary().Validate(); err != nil { t.Fatal(err) } if len(record.Message) != 3000 { t.Fatal("summary mutated legacy failure message") } }