package upstream import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcfg" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" ) func fullAPNSchema() map[string]map[string]bool { schema := make(map[string]map[string]bool, len(apnRequiredSchema)) for table, columns := range apnRequiredSchema { schema[table] = make(map[string]bool, len(columns)) for _, column := range columns { schema[table][column] = true } } return schema } // TestValidateAPNSchemaRequiresAllTablesAndColumns 固化依赖判据: // 缺表、缺列都必须给出可据此执行迁移的明确原因,而不是笼统的「未就绪」。 func TestValidateAPNSchemaRequiresAllTablesAndColumns(t *testing.T) { if err := validateAPNSchema(fullAPNSchema()); err != nil { t.Fatalf("完整结构不应报错:%v", err) } missingTable := fullAPNSchema() delete(missingTable, "data_operation_card_claim") err := validateAPNSchema(missingTable) if err == nil || !strings.Contains(err.Error(), "data_operation_card_claim") || !strings.Contains(err.Error(), "缺少数据表") { t.Fatalf("缺表未被识别:%v", err) } missingColumn := fullAPNSchema() delete(missingColumn["t_apn_operation"], "revision") err = validateAPNSchema(missingColumn) if err == nil || !strings.Contains(err.Error(), "t_apn_operation.revision") || !strings.Contains(err.Error(), "缺少字段") { t.Fatalf("缺列未被识别:%v", err) } // 表全部缺失时优先报缺表,避免一次报几十个字段干扰排查。 err = validateAPNSchema(map[string]map[string]bool{}) if err == nil || !strings.Contains(err.Error(), "缺少数据表") { t.Fatalf("空库应优先报缺表:%v", err) } } // TestAsyncApnSubmitBlockedWhenDependencyNotReady 验证依赖未就绪时在发请求前就拒绝, // 并把错误标记为「未发送」,避免上游误判为已办理。 func TestAsyncApnSubmitBlockedWhenDependencyNotReady(t *testing.T) { oldAdapter := g.Cfg().GetAdapter() defer g.Cfg().SetAdapter(oldAdapter) oldReady := apnDependencyReady defer func() { apnDependencyReady = oldReady }() apnDependencyReady = func(ctx context.Context) error { return errors.New("缺少数据表 data_operation_card_claim") } probes, submits := 0, 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/sim/query/sim-apn-capability" { probes++ return } submits++ })) defer server.Close() adapter, err := gcfg.NewAdapterContent(fmt.Sprintf(`{"upstream":{"proxy":{"address":%q,"timeout":"1s"}}}`, strings.TrimPrefix(server.URL, "http://"))) if err != nil { t.Fatal(err) } g.Cfg().SetAdapter(adapter) _, err = New().SubmitSimApnFunction(context.Background(), &model.SimCardApnChangeInput{ SimKeys: []*model.SimKey{{ChanKey: "test", ICCID: "fake-card"}}, ReqData: model.SimCardApnChangeInputItem{OperationID: "fake-operation"}, }) var notSent interface{ APNRequestNotSent() bool } if err == nil || !strings.Contains(err.Error(), "依赖未就绪") { t.Fatalf("依赖未就绪应拒绝提交:%v", err) } if !errors.As(err, ¬Sent) || !notSent.APNRequestNotSent() { t.Fatalf("依赖拒绝必须标记为未发送:%v", err) } if probes != 0 || submits != 0 { t.Fatalf("依赖未就绪时不应发出任何请求:probes=%d submits=%d", probes, submits) } } // TestReadinessCacheDoesNotCacheCanceledVerification 覆盖审查发现的场景: // 调用方取消导致的校验失败不能进进程级缓存,否则会误挡后续所有提交。 func TestReadinessCacheDoesNotCacheCanceledVerification(t *testing.T) { cache := &apnReadinessCache{ttl: time.Minute} calls := 0 cache.verify = func(ctx context.Context) error { calls++ if calls == 1 { return context.Canceled } return nil } if err := cache.check(context.Background()); !errors.Is(err, context.Canceled) { t.Fatalf("首次应返回取消错误:%v", err) } if err := cache.check(context.Background()); err != nil { t.Fatalf("取消不应被缓存,第二次应重新校验并放行:%v", err) } if calls != 2 { t.Fatalf("取消后应重新校验,实际调用%d次", calls) } } // TestReadinessCacheReusesResult 保证依赖检查不会每次提交都查库,同时迁移完成后能自动恢复。 func TestReadinessCacheReusesResult(t *testing.T) { cache := &apnReadinessCache{ttl: time.Minute} calls := 0 cache.verify = func(ctx context.Context) error { calls++ return nil } for i := 0; i < 3; i++ { if err := cache.check(context.Background()); err != nil { t.Fatalf("第%d次检查报错:%v", i+1, err) } } if calls != 1 { t.Fatalf("缓存期内应只查一次库,实际%d次", calls) } }