package simstatus import ( "context" "errors" "testing" zsetconsumer "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consumer/zset" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" queue "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/queue/zset" simstatusrepo "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/simstatus" ) type fakeStatusClient struct { responses [][]model.SimBaseRes[model.SimCardStatusItem] errors []error calls int } func (f *fakeStatusClient) QuerySimCardStatus(context.Context, *model.SimCardStatusInput) ([]model.SimBaseRes[model.SimCardStatusItem], error) { index := f.calls f.calls++ var err error if index < len(f.errors) { err = f.errors[index] } if err != nil { return nil, err } if index < len(f.responses) { return f.responses[index], nil } return nil, nil } type fakeStatusRepository struct { results []simstatusrepo.Result } func (f *fakeStatusRepository) ApplyBatch(_ context.Context, results []simstatusrepo.Result) error { f.results = append(f.results, results...) return nil } func TestHandlerUsesTaskCardWhenResponseOmitsSimKey(t *testing.T) { client := &fakeStatusClient{responses: [][]model.SimBaseRes[model.SimCardStatusItem]{{ {Code: 0, ResData: model.SimCardStatusItem{CardStatus: 2}}, }}} repo := &fakeStatusRepository{} handler, err := NewHandler(client, repo) if err != nil { t.Fatal(err) } task := queue.Task{CardID: 42, ICCID: "8986000000000042", ChanKey: "mobile-a", TempKey: "sim_yd_v5"} result := handler.Query(context.Background(), task) if !result.Success || result.CardStatus == nil || *result.CardStatus != 2 { t.Fatalf("unexpected result: %+v", result) } if err := handler.Apply(context.Background(), []zsetconsumer.Result{result}); err != nil { t.Fatal(err) } if len(repo.results) != 1 || repo.results[0].CardID != 42 || repo.results[0].CardStatus != 2 { t.Fatalf("unexpected updates: %+v", repo.results) } } func TestHandlerRetriesNetworkErrorsOnlyForConfiguredAttempts(t *testing.T) { client := &fakeStatusClient{ errors: []error{errors.New("temporary-1"), errors.New("temporary-2")}, responses: [][]model.SimBaseRes[model.SimCardStatusItem]{nil, nil, { {Code: 0, ResData: model.SimCardStatusItem{CardStatus: 4}}, }}, } handler, err := NewHandler(client, &fakeStatusRepository{}, 2) if err != nil { t.Fatal(err) } result := handler.Query(context.Background(), queue.Task{CardID: 1, ICCID: "iccid-1", ChanKey: "mobile-a", TempKey: "sim_yd_v5"}) if !result.Success || client.calls != 3 { t.Fatalf("success=%v calls=%d result=%+v", result.Success, client.calls, result) } } func TestHandlerClassifiesRetryableStatusFailures(t *testing.T) { tests := []struct { name string item model.SimBaseRes[model.SimCardStatusItem] tempKey string errorClass string retryable bool }{ { name: "telecom S00004", item: model.SimBaseRes[model.SimCardStatusItem]{Code: 9999, ErrCode: telecomRetryableCode}, tempKey: telecomStatusTemplate, errorClass: "carrier_retry", retryable: true, }, { name: "telecom v3 S00004", item: model.SimBaseRes[model.SimCardStatusItem]{Code: 9999, ErrCode: telecomRetryableCode}, tempKey: "sim_dx_v3", errorClass: "carrier_retry", retryable: true, }, { name: "gateway error without carrier code", item: model.SimBaseRes[model.SimCardStatusItem]{Code: chanGatewayInternalError}, tempKey: telecomStatusTemplate, errorClass: "gateway", retryable: true, }, { name: "token expired is retryable", item: model.SimBaseRes[model.SimCardStatusItem]{Code: tokenExpiredCode}, tempKey: "sim_yd_v5", errorClass: "token_expired", retryable: true, }, { name: "known business error remains terminal", item: model.SimBaseRes[model.SimCardStatusItem]{Code: 11011, ErrCode: "11011"}, tempKey: telecomStatusTemplate, errorClass: "business", retryable: false, }, { name: "non telecom S00004 remains terminal", item: model.SimBaseRes[model.SimCardStatusItem]{Code: 9999, ErrCode: telecomRetryableCode}, tempKey: "sim_yd_v5", errorClass: "business", retryable: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { client := &fakeStatusClient{responses: [][]model.SimBaseRes[model.SimCardStatusItem]{{test.item}}} handler, err := NewHandler(client, &fakeStatusRepository{}) if err != nil { t.Fatal(err) } result := handler.Query(context.Background(), queue.Task{CardID: 1, ICCID: "iccid-1", ChanKey: "telecom-a", TempKey: test.tempKey}) if result.ErrorClass != test.errorClass || result.Retryable != test.retryable || result.BlockChannel || result.Success { t.Fatalf("unexpected result: %+v", result) } }) } } func TestShouldRetryNextRoundUsesRetryableFlag(t *testing.T) { if !ShouldRetryNextRound(zsetconsumer.Result{Retryable: true}) { t.Fatal("retryable status failure should advance to the next round") } if ShouldRetryNextRound(zsetconsumer.Result{Success: true, Retryable: true}) || ShouldRetryNextRound(zsetconsumer.Result{}) { t.Fatal("successful and terminal status results must not advance rounds") } }