chore: 初始开源发布
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package upstreamapi
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestICCIDFromChannelCacheKey(t *testing.T) {
|
||||
tests := []struct{ key, want string }{
|
||||
{"sim:chan:iccid:898604F4152391115474", "898604F4152391115474"},
|
||||
{"sim:chan:iccid:8986112223600594371X", "8986112223600594371X"},
|
||||
{"sim:chan:iccid:", ""},
|
||||
{"unrelated:898604F4152391115474", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := iccidFromChannelCacheKey(tt.key); got != tt.want {
|
||||
t.Errorf("iccidFromChannelCacheKey(%q) = %q, want %q", tt.key, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultChannelTestTimeout = 2 * time.Minute
|
||||
defaultChannelTestItemTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type channelTestOperation struct {
|
||||
key string
|
||||
name string
|
||||
execute func(context.Context, model.SimCardChannelI, *model.SimKey, time.Time) (any, int, string, bool, error)
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) TestChannel(ctx context.Context, input *model.ChannelTestInput) (*model.ChannelTestOutput, error) {
|
||||
if input == nil {
|
||||
return nil, gerror.New("channel test input is required")
|
||||
}
|
||||
if input.ChannelID <= 0 || strings.TrimSpace(input.TenantID) == "" {
|
||||
return nil, gerror.New("channel is not accessible")
|
||||
}
|
||||
|
||||
operations, err := selectChannelTestOperations(input.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalTimeout := g.Cfg().MustGet(ctx, "upstreamapi.channel_test.timeout", defaultChannelTestTimeout).Duration()
|
||||
if totalTimeout <= 0 {
|
||||
totalTimeout = defaultChannelTestTimeout
|
||||
}
|
||||
itemTimeout := g.Cfg().MustGet(ctx, "upstreamapi.channel_test.item_timeout", defaultChannelTestItemTimeout).Duration()
|
||||
if itemTimeout <= 0 {
|
||||
itemTimeout = defaultChannelTestItemTimeout
|
||||
}
|
||||
testCtx, cancel := context.WithTimeout(ctx, totalTimeout)
|
||||
defer cancel()
|
||||
|
||||
config, err := service.ChannelConfig().LoadChannelForTest(testCtx, input.ChannelID, input.TenantID)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrChannelNotAccessible) {
|
||||
return nil, model.ErrChannelNotAccessible
|
||||
}
|
||||
return nil, gerror.Wrap(err, "load channel test configuration")
|
||||
}
|
||||
instance, err := u.newTestChannelInstance(testCtx, config)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "channel test configuration is invalid")
|
||||
}
|
||||
completionCtx, completionCancel := context.WithTimeout(testCtx, itemTimeout)
|
||||
simKey, err := resolveChannelTestSimKey(completionCtx, instance, config.Channel.ChanKey, input)
|
||||
completionCancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*model.ChannelTestItem, 0, len(operations))
|
||||
now := time.Now()
|
||||
for _, operation := range operations {
|
||||
if testCtx.Err() != nil {
|
||||
results = append(results, channelTestTimeoutResult(operation))
|
||||
continue
|
||||
}
|
||||
results = append(results, executeChannelTestOperation(testCtx, itemTimeout, operation, instance, simKey, now))
|
||||
}
|
||||
return &model.ChannelTestOutput{Items: results}, nil
|
||||
}
|
||||
|
||||
func resolveChannelTestSimKey(ctx context.Context, instance model.SimCardChannelI, chanKey string, input *model.ChannelTestInput) (*model.SimKey, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(input.InputMode))
|
||||
if mode == "" {
|
||||
mode = model.ChannelTestInputModeComplete
|
||||
}
|
||||
cardType := strings.ToLower(strings.TrimSpace(input.CardValueType))
|
||||
cardValue := strings.TrimSpace(input.CardValue)
|
||||
if mode == model.ChannelTestInputModeComplete {
|
||||
if strings.TrimSpace(input.ICCID) == "" || strings.TrimSpace(input.IMSI) == "" || strings.TrimSpace(input.MSISDN) == "" {
|
||||
return nil, gerror.New("complete mode requires iccid, imsi and msisdn")
|
||||
}
|
||||
return &model.SimKey{ChanKey: chanKey, ICCID: input.ICCID, IMSI: input.IMSI, MSISDN: input.MSISDN}, nil
|
||||
}
|
||||
if mode != model.ChannelTestInputModeSingle && mode != model.ChannelTestInputModeDirectSingle {
|
||||
return nil, gerror.New("unsupported channel test input mode")
|
||||
}
|
||||
if cardValue == "" || !validChannelTestCardType(cardType) {
|
||||
return nil, gerror.New("card value type and value are required")
|
||||
}
|
||||
key := &model.SimKey{ChanKey: chanKey}
|
||||
setChannelTestCardValue(key, cardType, cardValue)
|
||||
if mode == model.ChannelTestInputModeDirectSingle {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Card completion uses the same request-scoped instance as the selected
|
||||
// checks; it never falls back to chanMap or creates a second instance.
|
||||
basic, err := instance.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{SimKeys: []*model.SimKey{key}})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "complete card identifiers")
|
||||
}
|
||||
if len(basic) == 0 || basic[0] == nil || basic[0].Code != consts.ApiStatusSuccess {
|
||||
if len(basic) > 0 && basic[0] != nil && basic[0].Message != "" {
|
||||
return nil, gerror.New(basic[0].Message)
|
||||
}
|
||||
return nil, gerror.New("carrier did not return card identifiers")
|
||||
}
|
||||
completed := basic[0].ResData
|
||||
if strings.TrimSpace(completed.ICCID) == "" || strings.TrimSpace(completed.IMSI) == "" || strings.TrimSpace(completed.MSISDN) == "" {
|
||||
return nil, gerror.New("carrier did not return complete card identifiers")
|
||||
}
|
||||
return &model.SimKey{ChanKey: chanKey, ICCID: completed.ICCID, IMSI: completed.IMSI, MSISDN: completed.MSISDN}, nil
|
||||
}
|
||||
|
||||
func validChannelTestCardType(valueType string) bool {
|
||||
return valueType == "iccid" || valueType == "imsi" || valueType == "msisdn"
|
||||
}
|
||||
|
||||
func setChannelTestCardValue(key *model.SimKey, valueType, value string) {
|
||||
switch valueType {
|
||||
case "iccid":
|
||||
key.ICCID = value
|
||||
case "imsi":
|
||||
key.IMSI = value
|
||||
case "msisdn":
|
||||
key.MSISDN = value
|
||||
}
|
||||
}
|
||||
|
||||
func selectChannelTestOperations(requested []string) ([]channelTestOperation, error) {
|
||||
all := newChannelTestOperations()
|
||||
if len(requested) == 0 {
|
||||
return all, nil
|
||||
}
|
||||
wanted := make(map[string]struct{}, len(requested))
|
||||
for _, key := range requested {
|
||||
key = strings.TrimSpace(key)
|
||||
if _, ok := model.ChannelTestOperationNames[key]; !ok {
|
||||
return nil, gerror.Newf("unsupported channel test item: %s", key)
|
||||
}
|
||||
wanted[key] = struct{}{}
|
||||
}
|
||||
selected := make([]channelTestOperation, 0, len(wanted))
|
||||
for _, operation := range all {
|
||||
if _, ok := wanted[operation.key]; ok {
|
||||
selected = append(selected, operation)
|
||||
}
|
||||
}
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func executeChannelTestOperation(parent context.Context, itemTimeout time.Duration, operation channelTestOperation, instance model.SimCardChannelI, simKey *model.SimKey, now time.Time) *model.ChannelTestItem {
|
||||
started := time.Now()
|
||||
callCtx, cancel := context.WithTimeout(parent, itemTimeout)
|
||||
defer cancel()
|
||||
data, errorCode, message, success, err := operation.execute(callCtx, instance, simKey, now)
|
||||
result := &model.ChannelTestItem{Key: operation.key, Name: operation.name, Status: "failed", Data: data, ErrorCode: errorCode, DurationMs: time.Since(started).Milliseconds()}
|
||||
if callCtx.Err() != nil {
|
||||
result.Status = "timeout"
|
||||
result.Message = "channel test item timed out"
|
||||
return result
|
||||
}
|
||||
if err != nil {
|
||||
result.Message = err.Error()
|
||||
return result
|
||||
}
|
||||
if !success {
|
||||
result.Message = message
|
||||
return result
|
||||
}
|
||||
result.Status = "success"
|
||||
result.Message = message
|
||||
return result
|
||||
}
|
||||
|
||||
func channelTestTimeoutResult(operation channelTestOperation) *model.ChannelTestItem {
|
||||
return &model.ChannelTestItem{Key: operation.key, Name: operation.name, Status: "timeout", Message: "channel test request timed out"}
|
||||
}
|
||||
|
||||
func newChannelTestOperations() []channelTestOperation {
|
||||
testType := 0
|
||||
return []channelTestOperation{
|
||||
newChannelTestOperation("simBasicInfo", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimBasicInfoItem], error) {
|
||||
return instance.QuerySimBasicInfo(ctx, &model.SimBasicInfoInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simSession", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimSessionItem], error) {
|
||||
return instance.QuerySimSession(ctx, &model.SimSessionInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimCardStatusItem], error) {
|
||||
return instance.QuerySimCardStatus(ctx, &model.SimCardStatusInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simStopReason", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimStopReasonItem], error) {
|
||||
return instance.QuerySimStopReason(ctx, &model.SimStopReasonInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simFrozenState", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimFrozenStateItem], error) {
|
||||
return instance.QuerySimFrozenState(ctx, &model.SimFrozenStateInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simCardBindStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimCardBindStatusItem], error) {
|
||||
return instance.QuerySimCardBindStatus(ctx, &model.SimCardBindStatusInput{SimKeys: []*model.SimKey{key}, ReqData: model.SimCardBindStatusInputItem{TestType: &testType}})
|
||||
}),
|
||||
newChannelTestOperation("simDeviceStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimDeviceStatusItem], error) {
|
||||
return instance.QuerySimDeviceStatus(ctx, &model.SimDeviceStatusInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simRegionLimitStatus", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimRegionLimitStatusItem], error) {
|
||||
return instance.QuerySimRegionLimitStatus(ctx, &model.SimRegionLimitStatusInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simImei", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimImeiItem], error) {
|
||||
return instance.QuerySimImei(ctx, &model.SimImeiInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simDataUsage", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimDataUsageItem], error) {
|
||||
return instance.QuerySimDataUsage(ctx, &model.SimDataUsageInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
newChannelTestOperation("simDataUsageMonthly", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, now time.Time) ([]*model.SimBaseRes[model.SimDataUsageMonthlyOutputItem], error) {
|
||||
last := now.AddDate(0, -1, 0)
|
||||
return instance.QuerySimDataUsageMonthly(ctx, &model.SimDataUsageMonthlyInput{SimKeys: []*model.SimKey{key}, ReqData: model.SimDataUsageMonthlyInputItem{QueryDate: last.Format("200601")}})
|
||||
}),
|
||||
newChannelTestOperation("simDataUsageDaily", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, now time.Time) ([]*model.SimBaseRes[model.SimDataUsageDailyOutputItem], error) {
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
return instance.QuerySimDataUsageDaily(ctx, &model.SimDataUsageDailyInput{SimKeys: []*model.SimKey{key}, ReqData: model.SimDataUsageDailyInputItem{QueryDate: yesterday.Format("20060102")}})
|
||||
}),
|
||||
newChannelTestOperation("simApnInfo", func(ctx context.Context, instance model.SimCardChannelI, key *model.SimKey, _ time.Time) ([]*model.SimBaseRes[model.SimApnInfoItemList], error) {
|
||||
return instance.QuerySimApnInfo(ctx, &model.SimApnInfoInput{SimKeys: []*model.SimKey{key}})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func newChannelTestOperation[T any](key string, query func(context.Context, model.SimCardChannelI, *model.SimKey, time.Time) ([]*model.SimBaseRes[T], error)) channelTestOperation {
|
||||
return channelTestOperation{key: key, name: model.ChannelTestOperationNames[key], execute: func(ctx context.Context, instance model.SimCardChannelI, simKey *model.SimKey, now time.Time) (any, int, string, bool, error) {
|
||||
output, err := query(ctx, instance, simKey, now)
|
||||
if err != nil {
|
||||
return nil, 0, "", false, err
|
||||
}
|
||||
if len(output) == 0 || output[0] == nil {
|
||||
return nil, 0, "carrier returned no result", false, nil
|
||||
}
|
||||
first := output[0]
|
||||
if first.Code != consts.ApiStatusSuccess {
|
||||
return first.ResData, first.Code, first.Message, false, nil
|
||||
}
|
||||
return first.ResData, 0, first.Message, true, nil
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
||||
)
|
||||
|
||||
type channelTestConfigSource struct {
|
||||
service.IChannelConfig
|
||||
config *model.SimChannelConfigItemCache
|
||||
loadCall int
|
||||
}
|
||||
|
||||
func (s *channelTestConfigSource) LoadChannelForTest(_ context.Context, channelID int64, tenantID string) (*model.SimChannelConfigItemCache, error) {
|
||||
s.loadCall++
|
||||
if channelID != 42 || tenantID != "tenant-a" {
|
||||
return nil, model.ErrChannelNotAccessible
|
||||
}
|
||||
return s.config, nil
|
||||
}
|
||||
|
||||
type requestScopedTestChannel struct {
|
||||
model.SimCardChannelI
|
||||
basicCalls int
|
||||
sessionCalls int
|
||||
}
|
||||
|
||||
func (c *requestScopedTestChannel) QuerySimBasicInfo(_ context.Context, input *model.SimBasicInfoInput) (model.SimBasicInfoOutput, error) {
|
||||
c.basicCalls++
|
||||
return model.SimBasicInfoOutput{&model.SimBaseRes[model.SimBasicInfoItem]{
|
||||
Code: consts.ApiStatusSuccess,
|
||||
SimKey: input.SimKeys[0],
|
||||
ResData: model.SimBasicInfoItem{
|
||||
ICCID: "iccid-complete", IMSI: "imsi-complete", MSISDN: "msisdn-complete",
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c *requestScopedTestChannel) QuerySimSession(_ context.Context, input *model.SimSessionInput) (model.SimSessionOutput, error) {
|
||||
c.sessionCalls++
|
||||
return model.SimSessionOutput{&model.SimBaseRes[model.SimSessionItem]{
|
||||
Code: consts.ApiStatusSuccess, SimKey: input.SimKeys[0], ResData: model.SimSessionItem{Status: "0"},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
type requestScopedTestTemplate struct {
|
||||
calls int
|
||||
channel *requestScopedTestChannel
|
||||
}
|
||||
|
||||
func (t *requestScopedTestTemplate) NewTemp(context.Context, *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
t.calls++
|
||||
t.channel = &requestScopedTestChannel{}
|
||||
return t.channel
|
||||
}
|
||||
|
||||
func TestChannelUsesOneTemporaryInstanceWithoutRuntimeRegistration(t *testing.T) {
|
||||
abnormal := 1
|
||||
config := &model.SimChannelConfigItemCache{
|
||||
Channel: &entity.TSimChannels{Id: 42, TenantId: "tenant-a", ChanKey: "abnormal-channel", TempKey: "test-template"},
|
||||
Template: &entity.TSimChannelTemplate{TempKey: "test-template", TempModel: "direct"},
|
||||
IsNormal: &abnormal,
|
||||
}
|
||||
source := &channelTestConfigSource{config: config}
|
||||
oldConfig := service.ChannelConfig()
|
||||
service.RegisterChannelConfig(source)
|
||||
t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) })
|
||||
|
||||
api := NewUpstreamAPI()
|
||||
template := &requestScopedTestTemplate{}
|
||||
api.RegisterTemp("test-template", template)
|
||||
output, err := api.TestChannel(context.Background(), &model.ChannelTestInput{
|
||||
ChannelID: 42, TenantID: "tenant-a", InputMode: model.ChannelTestInputModeSingle,
|
||||
CardValueType: "iccid", CardValue: "iccid-only", Items: []string{"simBasicInfo", "simSession"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if source.loadCall != 1 || template.calls != 1 {
|
||||
t.Fatalf("config loads=%d instance builds=%d", source.loadCall, template.calls)
|
||||
}
|
||||
if template.channel.basicCalls != 2 || template.channel.sessionCalls != 1 {
|
||||
t.Fatalf("basic calls=%d session calls=%d", template.channel.basicCalls, template.channel.sessionCalls)
|
||||
}
|
||||
if len(output.Items) != 2 || output.Items[0].Status != "success" || output.Items[1].Status != "success" {
|
||||
t.Fatalf("unexpected test output: %+v", output)
|
||||
}
|
||||
if _, ok := api.GetChan("abnormal-channel"); ok {
|
||||
t.Fatal("temporary test instance entered the runtime channel map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelPreservesExistingRuntimeInstance(t *testing.T) {
|
||||
normal := 0
|
||||
config := &model.SimChannelConfigItemCache{
|
||||
Channel: &entity.TSimChannels{Id: 42, TenantId: "tenant-a", ChanKey: "normal-channel", TempKey: "test-template"},
|
||||
Template: &entity.TSimChannelTemplate{TempKey: "test-template", TempModel: "direct"},
|
||||
IsNormal: &normal,
|
||||
}
|
||||
source := &channelTestConfigSource{config: config}
|
||||
oldConfig := service.ChannelConfig()
|
||||
service.RegisterChannelConfig(source)
|
||||
t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) })
|
||||
|
||||
api := NewUpstreamAPI()
|
||||
runtimeInstance := &requestScopedTestChannel{}
|
||||
runtimeCache := &model.SimCardChannelCache{Instance: runtimeInstance, Version: "runtime-v1"}
|
||||
api.RegisterChan("normal-channel", runtimeCache.Version, runtimeCache)
|
||||
template := &requestScopedTestTemplate{}
|
||||
api.RegisterTemp("test-template", template)
|
||||
|
||||
_, err := api.TestChannel(context.Background(), &model.ChannelTestInput{
|
||||
ChannelID: 42, TenantID: "tenant-a", InputMode: model.ChannelTestInputModeComplete,
|
||||
ICCID: "i", IMSI: "m", MSISDN: "n", Items: []string{"simSession"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := api.GetChan("normal-channel")
|
||||
if !ok || got != runtimeCache || got.Instance != runtimeInstance {
|
||||
t.Fatal("channel test replaced the existing runtime instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelRejectsTenantMismatchAndMutationItems(t *testing.T) {
|
||||
normal := 0
|
||||
source := &channelTestConfigSource{config: &model.SimChannelConfigItemCache{
|
||||
Channel: &entity.TSimChannels{Id: 42, TenantId: "tenant-a", ChanKey: "channel", TempKey: "test-template"},
|
||||
Template: &entity.TSimChannelTemplate{TempKey: "test-template", TempModel: "direct"},
|
||||
IsNormal: &normal,
|
||||
}}
|
||||
oldConfig := service.ChannelConfig()
|
||||
service.RegisterChannelConfig(source)
|
||||
t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) })
|
||||
api := NewUpstreamAPI()
|
||||
api.RegisterTemp("test-template", &requestScopedTestTemplate{})
|
||||
|
||||
if _, err := api.TestChannel(context.Background(), &model.ChannelTestInput{ChannelID: 42, TenantID: "tenant-b"}); err == nil || err.Error() != "channel is not accessible" {
|
||||
t.Fatalf("tenant mismatch error = %v", err)
|
||||
}
|
||||
if _, err := api.TestChannel(context.Background(), &model.ChannelTestInput{
|
||||
ChannelID: 42, TenantID: "tenant-a", InputMode: model.ChannelTestInputModeComplete,
|
||||
ICCID: "i", IMSI: "m", MSISDN: "n", Items: []string{"changeSimStatus"},
|
||||
}); err == nil {
|
||||
t.Fatal("mutation item was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnregisterChanIsIdempotent(t *testing.T) {
|
||||
api := NewUpstreamAPI()
|
||||
api.UnregisterChan("missing")
|
||||
api.RegisterChan("channel", "v1", &model.SimCardChannelCache{Instance: &requestScopedTestChannel{}})
|
||||
api.UnregisterChan("channel")
|
||||
api.UnregisterChan("channel")
|
||||
if _, ok := api.GetChan("channel"); ok {
|
||||
t.Fatal("channel remains registered")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
historicalMonthLayout = "200601"
|
||||
historicalDailyLayout = "20060102"
|
||||
historicalMonthFormatError = "queryDate格式错误,应为yyyyMM"
|
||||
historicalMonthRangeError = "月流量仅支持查询已结束月份;本月累计流量请调用 /sim/query/sim-datausage"
|
||||
historicalDailyFormatError = "queryDate格式错误,应为yyyyMMdd"
|
||||
historicalDailyRangeError = "日流量仅支持查询已结束日期,不支持当天或未来日期"
|
||||
)
|
||||
|
||||
var historicalPeriodLocation = time.FixedZone("UTC+8", 8*60*60)
|
||||
|
||||
const currentBatchMonthRangeError = "电信月批量接口仅支持当月;历史月份请调用 /sim/query/sim-datausage-monthly"
|
||||
|
||||
func validateCurrentBatchMonth(queryDate string, now time.Time) string {
|
||||
queryDate = strings.TrimSpace(queryDate)
|
||||
if _, err := time.ParseInLocation(historicalMonthLayout, queryDate, historicalPeriodLocation); err != nil {
|
||||
return historicalMonthFormatError
|
||||
}
|
||||
if queryDate != now.In(historicalPeriodLocation).Format(historicalMonthLayout) {
|
||||
return currentBatchMonthRangeError
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateHistoricalMonth(queryDate string, now time.Time) string {
|
||||
queryDate = strings.TrimSpace(queryDate)
|
||||
if len(queryDate) != len(historicalMonthLayout) {
|
||||
return historicalMonthFormatError
|
||||
}
|
||||
if _, err := time.ParseInLocation(historicalMonthLayout, queryDate, historicalPeriodLocation); err != nil {
|
||||
return historicalMonthFormatError
|
||||
}
|
||||
if queryDate >= now.In(historicalPeriodLocation).Format(historicalMonthLayout) {
|
||||
return historicalMonthRangeError
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateHistoricalDay(queryDate string, now time.Time) string {
|
||||
queryDate = strings.TrimSpace(queryDate)
|
||||
if len(queryDate) != len(historicalDailyLayout) {
|
||||
return historicalDailyFormatError
|
||||
}
|
||||
if _, err := time.ParseInLocation(historicalDailyLayout, queryDate, historicalPeriodLocation); err != nil {
|
||||
return historicalDailyFormatError
|
||||
}
|
||||
if queryDate >= now.In(historicalPeriodLocation).Format(historicalDailyLayout) {
|
||||
return historicalDailyRangeError
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/logic/channelconfig"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type normalGateTestChannel struct {
|
||||
model.SimCardChannelI
|
||||
}
|
||||
|
||||
type normalGateTestTemplate struct {
|
||||
calls int
|
||||
lastConfig *model.SimChannelConfigItemCache
|
||||
}
|
||||
|
||||
func (f *normalGateTestTemplate) NewTemp(_ context.Context, cfg *model.SimChannelConfigItemCache) model.SimCardChannelI {
|
||||
f.calls++
|
||||
f.lastConfig = cfg
|
||||
return &normalGateTestChannel{}
|
||||
}
|
||||
|
||||
// Exercise real ORM decoding and config versioning as well as both creation paths.
|
||||
// All SQL runs against a temporary SQLite file, never a configured environment DB.
|
||||
func TestChannelNormalCreationGate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{
|
||||
Type: "sqlite", Name: filepath.Join(t.TempDir(), "channels.db"),
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := g.DB()
|
||||
t.Cleanup(func() { _ = db.Close(ctx) })
|
||||
execSQL := func(query string, args ...any) {
|
||||
t.Helper()
|
||||
if _, err := db.Exec(ctx, query, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
execSQL("CREATE TABLE t_sim_channels (id INTEGER PRIMARY KEY, chan_key TEXT, temp_key TEXT, chan_name TEXT, is_normal INTEGER NULL)")
|
||||
execSQL("CREATE TABLE t_sim_channel_template (id INTEGER PRIMARY KEY, temp_key TEXT, temp_model TEXT)")
|
||||
execSQL("INSERT INTO t_sim_channel_template (id, temp_key, temp_model) VALUES (1, 'gate_test', 'direct')")
|
||||
|
||||
oldConfig := service.ChannelConfig()
|
||||
t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) })
|
||||
|
||||
for _, path := range []string{"loop", "refresh"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value any
|
||||
eligible bool
|
||||
}{
|
||||
{"yes_is_zero", 0, true},
|
||||
{"no_is_one", 1, false},
|
||||
{"null", nil, false},
|
||||
{"unexpected", 2, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
execSQL("DELETE FROM t_sim_channels")
|
||||
execSQL("INSERT INTO t_sim_channels (id, chan_key, temp_key, is_normal) VALUES (1, 'gate_channel', 'gate_test', ?)", tc.value)
|
||||
config := channelconfig.NewChannelConfig()
|
||||
service.RegisterChannelConfig(config)
|
||||
api := NewUpstreamAPI()
|
||||
factory := &normalGateTestTemplate{}
|
||||
api.RegisterTemp("gate_test", factory)
|
||||
reload := func() {
|
||||
t.Helper()
|
||||
var err error
|
||||
if path == "loop" {
|
||||
err = config.LoadAllChannelConfig(ctx)
|
||||
if err == nil {
|
||||
err = api.loop(ctx)
|
||||
}
|
||||
} else {
|
||||
err = api.RefreshChannel(ctx, "gate_channel")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("refresh must succeed: %v", err)
|
||||
}
|
||||
}
|
||||
reload()
|
||||
cfg, ok := config.GetChannel("gate_channel")
|
||||
if !ok || cfg.CanCreateInstance() != tc.eligible {
|
||||
t.Fatalf("wrong eligibility after ORM scan: %+v", cfg)
|
||||
}
|
||||
if tc.value == nil && cfg.IsNormal != nil {
|
||||
t.Fatalf("SQL NULL decoded as non-nil: %v", *cfg.IsNormal)
|
||||
}
|
||||
if cfg.Channel.ChanKey != "gate_channel" {
|
||||
t.Fatal("embedded channel fields were not scanned")
|
||||
}
|
||||
_, exists := api.GetChan("gate_channel")
|
||||
if exists != tc.eligible || (factory.calls == 1) != tc.eligible {
|
||||
t.Fatalf("instance exists=%v factory calls=%d eligible=%v", exists, factory.calls, tc.eligible)
|
||||
}
|
||||
initialVersion := cfg.Version
|
||||
|
||||
// Only is_normal changes: the configuration cache must notice it.
|
||||
execSQL("UPDATE t_sim_channels SET is_normal=0")
|
||||
reload()
|
||||
current, ok := api.GetChan("gate_channel")
|
||||
if !ok || current.Instance == nil {
|
||||
t.Fatal("eligible channel was not instantiated")
|
||||
}
|
||||
cfg, _ = config.GetChannel("gate_channel")
|
||||
if !tc.eligible && cfg.Version == initialVersion {
|
||||
t.Fatal("changing only is_normal did not change config version")
|
||||
}
|
||||
oldCalls := factory.calls
|
||||
|
||||
// Becoming ineligible must remove the old runtime instance.
|
||||
execSQL("UPDATE t_sim_channels SET is_normal=1, chan_name='changed while disabled', temp_key='missing_template'")
|
||||
reload()
|
||||
if _, exists := api.GetChan("gate_channel"); exists || factory.calls != oldCalls {
|
||||
t.Fatal("ineligible channel retained or rebuilt its runtime instance")
|
||||
}
|
||||
cfg, _ = config.GetChannel("gate_channel")
|
||||
if cfg.CanCreateInstance() {
|
||||
t.Fatal("ineligible config must remain ineligible")
|
||||
}
|
||||
// Repeat the disabled refresh, as a retry would.
|
||||
reload()
|
||||
if factory.calls != oldCalls {
|
||||
t.Fatal("retry created an ineligible instance")
|
||||
}
|
||||
|
||||
// A process restart cannot recreate the retained disabled instance.
|
||||
restarted := NewUpstreamAPI()
|
||||
restarted.RegisterTemp("gate_test", factory)
|
||||
if err := restarted.loop(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := restarted.GetChan("gate_channel"); ok {
|
||||
t.Fatal("restart recreated disabled instance")
|
||||
}
|
||||
|
||||
execSQL("UPDATE t_sim_channels SET is_normal=0, temp_key='gate_test'")
|
||||
reload()
|
||||
restored, _ := api.GetChan("gate_channel")
|
||||
if restored == current || factory.calls != oldCalls+1 ||
|
||||
factory.lastConfig.Channel.ChanName != "changed while disabled" {
|
||||
t.Fatal("restored channel did not rebuild using the latest configuration")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/service"
|
||||
)
|
||||
|
||||
type staleLoopConfigSource struct {
|
||||
service.IChannelConfig
|
||||
snapshot *model.SimChannelConfigItemCache
|
||||
latest *model.SimChannelConfigItemCache
|
||||
}
|
||||
|
||||
func (s *staleLoopConfigSource) GetAllChannel(context.Context) ([]*model.SimChannelConfigItemCache, error) {
|
||||
return []*model.SimChannelConfigItemCache{s.snapshot}, nil
|
||||
}
|
||||
|
||||
func (s *staleLoopConfigSource) GetChannel(string) (*model.SimChannelConfigItemCache, bool) {
|
||||
return s.latest, true
|
||||
}
|
||||
|
||||
func TestLoopDoesNotReviveChannelFromStaleNormalSnapshot(t *testing.T) {
|
||||
normal, abnormal := 0, 1
|
||||
snapshot := &model.SimChannelConfigItemCache{
|
||||
Channel: &entity.TSimChannels{ChanKey: "channel", TempKey: "template"},
|
||||
Template: &entity.TSimChannelTemplate{TempKey: "template", TempModel: "direct"},
|
||||
IsNormal: &normal,
|
||||
Version: "old-normal",
|
||||
}
|
||||
latest := &model.SimChannelConfigItemCache{
|
||||
Channel: &entity.TSimChannels{ChanKey: "channel", TempKey: "template"},
|
||||
IsNormal: &abnormal,
|
||||
Version: "new-abnormal",
|
||||
}
|
||||
source := &staleLoopConfigSource{snapshot: snapshot, latest: latest}
|
||||
oldConfig := service.ChannelConfig()
|
||||
service.RegisterChannelConfig(source)
|
||||
t.Cleanup(func() { service.RegisterChannelConfig(oldConfig) })
|
||||
|
||||
api := NewUpstreamAPI()
|
||||
api.RegisterChan("channel", "old-normal", &model.SimCardChannelCache{Instance: &requestScopedTestChannel{}, Version: "old-normal"})
|
||||
api.RegisterTemp("template", &requestScopedTestTemplate{})
|
||||
if err := api.loop(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := api.GetChan("channel"); ok {
|
||||
t.Fatal("periodic loop revived or retained an abnormal channel from a stale snapshot")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
package upstreamapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-chan/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (u *sUpstreamAPI) QuerySimCardStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (output model.SimCardStatusOutput, err error) {
|
||||
return u.queryStatusBatch(ctx, input)
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) QuerySimDataUsageMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (output model.SimDataUsageMonthlyOutput, err error) {
|
||||
return u.queryMonthlyBatch(ctx, input)
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) QuerySimDataUsageDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (output model.SimDataUsageDailyOutput, err error) {
|
||||
return u.queryDailyBatch(ctx, input)
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) queryStatusBatch(ctx context.Context, input *model.SimCardStatusInput) (output model.SimCardStatusOutput, err error) {
|
||||
output = make(model.SimCardStatusOutput, 0)
|
||||
if input == nil || len(input.SimKeys) == 0 {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
inputMap := make(map[string]*model.SimCardStatusInput)
|
||||
chanKeyMap, err := u.packChanKey(ctx, input.SimKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for chanKey, simKeys := range chanKeyMap {
|
||||
inputMap[chanKey] = &model.SimCardStatusInput{
|
||||
SimKeys: simKeys,
|
||||
ReqData: input.ReqData,
|
||||
}
|
||||
}
|
||||
|
||||
for chanKey, inputNew := range inputMap {
|
||||
if chanKey == "" {
|
||||
output = appendBatchStatusFailures(output, inputNew.SimKeys, "通道未配置")
|
||||
continue
|
||||
}
|
||||
channel, ok := u.GetChan(chanKey)
|
||||
if !ok || channel.Instance == nil {
|
||||
output = appendBatchStatusFailures(output, inputNew.SimKeys, "通道未实例化")
|
||||
continue
|
||||
}
|
||||
batchChannel, ok := channel.Instance.(model.SimCardBatchQueryI)
|
||||
if !ok {
|
||||
output = appendBatchStatusFailures(output, inputNew.SimKeys, "通道不支持批量查询")
|
||||
continue
|
||||
}
|
||||
|
||||
out, err := batchChannel.QuerySimCardStatusBatch(ctx, inputNew)
|
||||
if err != nil {
|
||||
output = appendBatchStatusFailures(output, inputNew.SimKeys, err.Error())
|
||||
continue
|
||||
}
|
||||
output = append(output, out...)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) queryMonthlyBatch(ctx context.Context, input *model.SimDataUsageMonthlyInput) (output model.SimDataUsageMonthlyOutput, err error) {
|
||||
output = make(model.SimDataUsageMonthlyOutput, 0)
|
||||
if input == nil || len(input.SimKeys) == 0 {
|
||||
return output, nil
|
||||
}
|
||||
if message := validateCurrentBatchMonth(input.ReqData.QueryDate, time.Now()); message != "" {
|
||||
return appendBatchMonthlyFailures(output, input.SimKeys, message), nil
|
||||
}
|
||||
|
||||
inputMap := make(map[string]*model.SimDataUsageMonthlyInput)
|
||||
chanKeyMap, err := u.packChanKey(ctx, input.SimKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for chanKey, simKeys := range chanKeyMap {
|
||||
inputMap[chanKey] = &model.SimDataUsageMonthlyInput{
|
||||
SimKeys: simKeys,
|
||||
ReqData: input.ReqData,
|
||||
}
|
||||
}
|
||||
|
||||
for chanKey, inputNew := range inputMap {
|
||||
if chanKey == "" {
|
||||
output = appendBatchMonthlyFailures(output, inputNew.SimKeys, "通道未配置")
|
||||
continue
|
||||
}
|
||||
channel, ok := u.GetChan(chanKey)
|
||||
if !ok || channel.Instance == nil {
|
||||
output = appendBatchMonthlyFailures(output, inputNew.SimKeys, "通道未实例化")
|
||||
continue
|
||||
}
|
||||
batchChannel, ok := channel.Instance.(model.SimCardBatchQueryI)
|
||||
if !ok {
|
||||
output = appendBatchMonthlyFailures(output, inputNew.SimKeys, "通道不支持批量查询")
|
||||
continue
|
||||
}
|
||||
|
||||
out, err := batchChannel.QuerySimDataUsageMonthlyBatch(ctx, inputNew)
|
||||
if err != nil {
|
||||
output = appendBatchMonthlyFailures(output, inputNew.SimKeys, err.Error())
|
||||
continue
|
||||
}
|
||||
output = append(output, out...)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (u *sUpstreamAPI) queryDailyBatch(ctx context.Context, input *model.SimDataUsageDailyInput) (output model.SimDataUsageDailyOutput, err error) {
|
||||
output = make(model.SimDataUsageDailyOutput, 0)
|
||||
if input == nil || len(input.SimKeys) == 0 {
|
||||
return output, nil
|
||||
}
|
||||
if message := validateHistoricalDay(input.ReqData.QueryDate, time.Now()); message != "" {
|
||||
return appendBatchDailyFailures(output, input.SimKeys, message), nil
|
||||
}
|
||||
|
||||
inputMap := make(map[string]*model.SimDataUsageDailyInput)
|
||||
chanKeyMap, err := u.packChanKey(ctx, input.SimKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for chanKey, simKeys := range chanKeyMap {
|
||||
inputMap[chanKey] = &model.SimDataUsageDailyInput{
|
||||
SimKeys: simKeys,
|
||||
ReqData: input.ReqData,
|
||||
}
|
||||
}
|
||||
|
||||
for chanKey, inputNew := range inputMap {
|
||||
if chanKey == "" {
|
||||
output = appendBatchDailyFailures(output, inputNew.SimKeys, "通道未配置")
|
||||
continue
|
||||
}
|
||||
channel, ok := u.GetChan(chanKey)
|
||||
if !ok || channel.Instance == nil {
|
||||
output = appendBatchDailyFailures(output, inputNew.SimKeys, "通道未实例化")
|
||||
continue
|
||||
}
|
||||
batchChannel, ok := channel.Instance.(model.SimCardBatchQueryI)
|
||||
if !ok {
|
||||
output = appendBatchDailyFailures(output, inputNew.SimKeys, "通道不支持批量查询")
|
||||
continue
|
||||
}
|
||||
|
||||
out, err := batchChannel.QuerySimDataUsageDailyBatch(ctx, inputNew)
|
||||
if err != nil {
|
||||
output = appendBatchDailyFailures(output, inputNew.SimKeys, err.Error())
|
||||
continue
|
||||
}
|
||||
output = append(output, out...)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func appendBatchStatusFailures(output model.SimCardStatusOutput, simKeys []*model.SimKey, msg string) model.SimCardStatusOutput {
|
||||
for _, simKey := range simKeys {
|
||||
output = append(output, &model.SimBaseRes[model.SimCardStatusItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func appendBatchMonthlyFailures(output model.SimDataUsageMonthlyOutput, simKeys []*model.SimKey, msg string) model.SimDataUsageMonthlyOutput {
|
||||
for _, simKey := range simKeys {
|
||||
output = append(output, &model.SimBaseRes[model.SimDataUsageMonthlyOutputItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func appendBatchDailyFailures(output model.SimDataUsageDailyOutput, simKeys []*model.SimKey, msg string) model.SimDataUsageDailyOutput {
|
||||
for _, simKey := range simKeys {
|
||||
output = append(output, &model.SimBaseRes[model.SimDataUsageDailyOutputItem]{
|
||||
SimKey: simKey,
|
||||
Code: consts.ApiStatusError,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
||||
Reference in New Issue
Block a user