package upstreamapi import ( "context" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model" ) type historyUsageTestChannel struct { model.SimCardChannelI monthlyCalls int dailyCalls int monthlyBatchCalls int dailyBatchCalls int currentCalls int } func (c *historyUsageTestChannel) QuerySimDataUsageMonthly(_ context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) { c.monthlyCalls++ output := make(model.SimDataUsageMonthlyOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { output = append(output, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, }) } return output, nil } func (c *historyUsageTestChannel) QuerySimDataUsageDaily(_ context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) { c.dailyCalls++ output := make(model.SimDataUsageDailyOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { output = append(output, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, }) } return output, nil } func (c *historyUsageTestChannel) QuerySimDataUsageMonthlyBatch(_ context.Context, input *model.SimDataUsageMonthlyInput) (model.SimDataUsageMonthlyOutput, error) { c.monthlyBatchCalls++ return c.QuerySimDataUsageMonthly(context.Background(), input) } func (c *historyUsageTestChannel) QuerySimDataUsageDailyBatch(_ context.Context, input *model.SimDataUsageDailyInput) (model.SimDataUsageDailyOutput, error) { c.dailyBatchCalls++ return c.QuerySimDataUsageDaily(context.Background(), input) } func (c *historyUsageTestChannel) QuerySimCardStatusBatch(_ context.Context, _ *model.SimCardStatusInput) (model.SimCardStatusOutput, error) { return nil, nil } func (c *historyUsageTestChannel) QuerySimDataUsage(_ context.Context, input *model.SimDataUsageInput) (model.SimDataUsageOutput, error) { c.currentCalls++ output := make(model.SimDataUsageOutput, 0, len(input.SimKeys)) for _, simKey := range input.SimKeys { output = append(output, &model.SimBaseRes[model.SimDataUsageItem]{ SimKey: simKey, Code: consts.ApiStatusSuccess, }) } return output, nil } func TestValidateHistoricalPeriodAtUTC8Boundary(t *testing.T) { now := time.Date(2026, time.September, 2, 16, 0, 0, 0, time.UTC) monthCases := []struct { name string queryDate string want string }{ {name: "previous month", queryDate: "202608", want: ""}, {name: "current month", queryDate: "202609", want: historicalMonthRangeError}, {name: "future month", queryDate: "202610", want: historicalMonthRangeError}, {name: "empty month", queryDate: "", want: historicalMonthFormatError}, {name: "hyphenated month", queryDate: "2026-08", want: historicalMonthFormatError}, {name: "invalid month", queryDate: "202613", want: historicalMonthFormatError}, } for _, tc := range monthCases { t.Run("month "+tc.name, func(t *testing.T) { if got := validateHistoricalMonth(tc.queryDate, now); got != tc.want { t.Fatalf("validateHistoricalMonth(%q) = %q, want %q", tc.queryDate, got, tc.want) } }) } batchMonthCases := []struct { name string queryDate string want string }{ {name: "previous month", queryDate: "202608", want: currentBatchMonthRangeError}, {name: "current month", queryDate: "202609", want: ""}, {name: "future month", queryDate: "202610", want: currentBatchMonthRangeError}, {name: "empty month", queryDate: "", want: historicalMonthFormatError}, {name: "invalid month", queryDate: "202613", want: historicalMonthFormatError}, } for _, tc := range batchMonthCases { t.Run("batch month "+tc.name, func(t *testing.T) { if got := validateCurrentBatchMonth(tc.queryDate, now); got != tc.want { t.Fatalf("validateCurrentBatchMonth(%q) = %q, want %q", tc.queryDate, got, tc.want) } }) } dayCases := []struct { name string queryDate string want string }{ {name: "previous day", queryDate: "20260902", want: ""}, {name: "current day", queryDate: "20260903", want: historicalDailyRangeError}, {name: "future day", queryDate: "20260904", want: historicalDailyRangeError}, {name: "empty day", queryDate: "", want: historicalDailyFormatError}, {name: "hyphenated day", queryDate: "2026-09-02", want: historicalDailyFormatError}, {name: "invalid day", queryDate: "20260230", want: historicalDailyFormatError}, } for _, tc := range dayCases { t.Run("day "+tc.name, func(t *testing.T) { if got := validateHistoricalDay(tc.queryDate, now); got != tc.want { t.Fatalf("validateHistoricalDay(%q) = %q, want %q", tc.queryDate, got, tc.want) } }) } } func TestValidateHistoricalPeriodAcrossMonthEnd(t *testing.T) { beforeMidnight := time.Date(2026, time.August, 31, 15, 59, 59, 0, time.UTC) afterMidnight := time.Date(2026, time.August, 31, 16, 0, 0, 0, time.UTC) if got := validateHistoricalMonth("202608", beforeMidnight); got != historicalMonthRangeError { t.Fatalf("current month before UTC+8 month end = %q", got) } if got := validateHistoricalDay("20260831", beforeMidnight); got != historicalDailyRangeError { t.Fatalf("current day before UTC+8 month end = %q", got) } if got := validateHistoricalMonth("202608", afterMidnight); got != "" { t.Fatalf("completed month after UTC+8 month end = %q", got) } if got := validateHistoricalDay("20260831", afterMidnight); got != "" { t.Fatalf("completed day after UTC+8 month end = %q", got) } if got := validateCurrentBatchMonth("202608", beforeMidnight); got != "" { t.Fatalf("current batch month before UTC+8 month end = %q", got) } if got := validateCurrentBatchMonth("202608", afterMidnight); got != currentBatchMonthRangeError { t.Fatalf("previous batch month after UTC+8 month end = %q", got) } if got := validateCurrentBatchMonth("202609", afterMidnight); got != "" { t.Fatalf("current batch month after UTC+8 month end = %q", got) } } func TestHistoricalPeriodValidationSkipsChannels(t *testing.T) { ctx := context.Background() api := NewUpstreamAPI() channel := &historyUsageTestChannel{} api.RegisterChan("test_channel", "v1", &model.SimCardChannelCache{Instance: channel}) simKeys := []*model.SimKey{ {ChanKey: "test_channel", ICCID: "iccid-1"}, {ChanKey: "test_channel", ICCID: "iccid-2"}, } now := time.Now().In(historicalPeriodLocation) monthly, err := api.QuerySimDataUsageMonthly(ctx, &model.SimDataUsageMonthlyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: now.Format("200601")}, }) if err != nil { t.Fatalf("QuerySimDataUsageMonthly returned error: %v", err) } assertMonthlyFailures(t, monthly, simKeys, historicalMonthRangeError) daily, err := api.QuerySimDataUsageDaily(ctx, &model.SimDataUsageDailyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: now.Format("20060102")}, }) if err != nil { t.Fatalf("QuerySimDataUsageDaily returned error: %v", err) } assertDailyFailures(t, daily, simKeys, historicalDailyRangeError) monthlyBatch, err := api.QuerySimDataUsageMonthlyBatch(ctx, &model.SimDataUsageMonthlyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: "999912"}, }) if err != nil { t.Fatalf("QuerySimDataUsageMonthlyBatch returned error: %v", err) } assertMonthlyFailures(t, monthlyBatch, simKeys, currentBatchMonthRangeError) dailyBatch, err := api.QuerySimDataUsageDailyBatch(ctx, &model.SimDataUsageDailyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: "99991231"}, }) if err != nil { t.Fatalf("QuerySimDataUsageDailyBatch returned error: %v", err) } assertDailyFailures(t, dailyBatch, simKeys, historicalDailyRangeError) invalidMonthly, err := api.QuerySimDataUsageMonthly(ctx, &model.SimDataUsageMonthlyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: "2026-08"}, }) if err != nil { t.Fatalf("invalid monthly query returned error: %v", err) } assertMonthlyFailures(t, invalidMonthly, simKeys, historicalMonthFormatError) invalidDaily, err := api.QuerySimDataUsageDailyBatch(ctx, &model.SimDataUsageDailyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: ""}, }) if err != nil { t.Fatalf("invalid daily batch query returned error: %v", err) } assertDailyFailures(t, invalidDaily, simKeys, historicalDailyFormatError) if channel.monthlyCalls != 0 || channel.dailyCalls != 0 || channel.monthlyBatchCalls != 0 || channel.dailyBatchCalls != 0 { t.Fatalf("rejected periods called channel: %+v", channel) } } func TestHistoricalPeriodsRouteAndCurrentUsageRemainsAvailable(t *testing.T) { ctx := context.Background() api := NewUpstreamAPI() channel := &historyUsageTestChannel{} api.RegisterChan("test_channel", "v1", &model.SimCardChannelCache{Instance: channel}) simKeys := []*model.SimKey{{ChanKey: "test_channel", ICCID: "iccid-1"}} currentMonth := time.Now().In(historicalPeriodLocation).Format(historicalMonthLayout) if _, err := api.QuerySimDataUsageMonthly(ctx, &model.SimDataUsageMonthlyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: "200001"}, }); err != nil { t.Fatalf("historical monthly query returned error: %v", err) } if _, err := api.QuerySimDataUsageDaily(ctx, &model.SimDataUsageDailyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: "20000101"}, }); err != nil { t.Fatalf("historical daily query returned error: %v", err) } if _, err := api.QuerySimDataUsageMonthlyBatch(ctx, &model.SimDataUsageMonthlyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: currentMonth}, }); err != nil { t.Fatalf("current monthly batch query returned error: %v", err) } if _, err := api.QuerySimDataUsageDailyBatch(ctx, &model.SimDataUsageDailyInput{ SimKeys: simKeys, ReqData: model.SimDataUsageDailyInputItem{QueryDate: "20000101"}, }); err != nil { t.Fatalf("historical daily batch query returned error: %v", err) } if _, err := api.QuerySimDataUsage(ctx, &model.SimDataUsageInput{SimKeys: simKeys}); err != nil { t.Fatalf("current usage query returned error: %v", err) } if channel.monthlyCalls != 2 || channel.dailyCalls != 2 || channel.monthlyBatchCalls != 1 || channel.dailyBatchCalls != 1 || channel.currentCalls != 1 { t.Fatalf("unexpected channel calls: %+v", channel) } } func assertMonthlyFailures(t *testing.T, output model.SimDataUsageMonthlyOutput, simKeys []*model.SimKey, message string) { t.Helper() if len(output) != len(simKeys) { t.Fatalf("monthly output length = %d, want %d", len(output), len(simKeys)) } for i, item := range output { if item.Code != consts.ApiStatusError || item.Message != message || item.SimKey != simKeys[i] { t.Fatalf("monthly output[%d] = %+v", i, item) } } } func assertDailyFailures(t *testing.T, output model.SimDataUsageDailyOutput, simKeys []*model.SimKey, message string) { t.Helper() if len(output) != len(simKeys) { t.Fatalf("daily output length = %d, want %d", len(output), len(simKeys)) } for i, item := range output { if item.Code != consts.ApiStatusError || item.Message != message || item.SimKey != simKeys[i] { t.Fatalf("daily output[%d] = %+v", i, item) } } }