package upstreamimpl import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "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" ) // yunzeExpire renders a wall-clock expiry in the operator-token wire format // (Asia/Shanghai, "2006-01-02 15:04:05"). func yunzeExpire() string { return time.Now().In(yunzeTokenLocation).Add(10 * time.Minute).Format(yunzeTokenExpiryLayout) } func TestYunzeTokenProviderFetch(t *testing.T) { expire := time.Now().In(yunzeTokenLocation).Add(10 * time.Minute) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("method = %s, want POST", r.Method) } if r.URL.Path != "/openapi/v3/query/operator-token" { t.Fatalf("path = %s", r.URL.Path) } if got := r.Header.Get("Content-Type"); got != "application/json" { t.Fatalf("content type = %q", got) } var request yunzeTokenRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) } if request.AppID != "app-id" || request.ChanKey != "249" { t.Fatalf("request = %+v", request) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"code":0,"message":"","data":{"chanAlias":"中国移动物联网","chanKey":"249","accessToken":"token-1","tokenExpire":"` + expire.Format(yunzeTokenExpiryLayout) + `"},"error":null}`)) })) defer server.Close() provider := NewYunzeTokenProvider(YunzeTokenConfig{ BaseURL: server.URL, Path: "/openapi/v3/query/operator-token", AppID: "app-id", Timeout: time.Second, }) token, err := provider.Fetch(context.Background(), "249") if err != nil { t.Fatalf("fetch token: %v", err) } if token.Value != "token-1" { t.Fatalf("token = %q", token.Value) } if token.ExpiresAt.IsZero() { t.Fatal("token expiry is empty") } if token.ExpiresAt.Sub(expire).Abs() > time.Second { t.Fatalf("expiry = %s, want %s", token.ExpiresAt, expire) } } // Pins the wire field name to the live endpoint. The published documentation // lists data.accessTokenExpire, but the endpoint returns data.tokenExpire; if // either side changes, this test fails and forces a deliberate decision. func TestYunzeTokenProviderExpiryFieldName(t *testing.T) { expire := yunzeExpire() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"code":0,"data":{"accessToken":"token-1","tokenExpire":"` + expire + `"},"error":null}`)) })) defer server.Close() provider := NewYunzeTokenProvider(YunzeTokenConfig{BaseURL: server.URL, AppID: "app-id", Timeout: time.Second}) token, err := provider.Fetch(context.Background(), "249") if err != nil { t.Fatalf("fetch token: %v", err) } if token.Value != "token-1" || token.ExpiresAt.IsZero() { t.Fatalf("token = %+v", token) } } func TestYunzeTokenProviderAcceptsStringCode(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"code":"0","data":{"accessToken":"token-str","tokenExpire":"` + yunzeExpire() + `"},"error":null}`)) })) defer server.Close() provider := NewYunzeTokenProvider(YunzeTokenConfig{BaseURL: server.URL, AppID: "app-id", Timeout: time.Second}) token, err := provider.Fetch(context.Background(), "249") if err != nil { t.Fatalf("fetch token: %v", err) } if token.Value != "token-str" { t.Fatalf("token = %q, want token-str", token.Value) } } func TestYunzeTokenProviderRejectsFailedResponse(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"code":1001,"message":"通道不存在","data":null,"error":null}`)) })) defer server.Close() provider := NewYunzeTokenProvider(YunzeTokenConfig{BaseURL: server.URL, AppID: "app-id", Timeout: time.Second}) if _, err := provider.Fetch(context.Background(), "249"); err == nil { t.Fatal("expected failed response error") } } func TestYunzeTokenProviderRejectsErrorPayload(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"code":0,"data":{"accessToken":"token-1","tokenExpire":"` + yunzeExpire() + `"},"error":{"msg":"account frozen"}}`)) })) defer server.Close() provider := NewYunzeTokenProvider(YunzeTokenConfig{BaseURL: server.URL, AppID: "app-id", Timeout: time.Second}) if _, err := provider.Fetch(context.Background(), "249"); err == nil { t.Fatal("expected error payload rejection") } } func TestParseSharedOneLinkConfigBusinessURL(t *testing.T) { newConfig := func(tempConfig string) *model.SimChannelConfigItemCache { return &model.SimChannelConfigItemCache{ Template: &entity.TSimChannelTemplate{TempConfig: tempConfig}, Channel: &entity.TSimChannels{ ChanKey: "249", AppKey: "app-id", ChanConfig: "{}", }, } } t.Run("defaults to shared OneLink URL", func(t *testing.T) { config, api, err := parseSharedOneLinkConfig(context.Background(), newConfig( `{"token":{"provider":"yunze","baseUrl":"http://apiv3.example.com","path":"/route/open/sharedToken"}}`, )) if err != nil { t.Fatalf("parse config: %v", err) } if config.Provider != "yunze" || config.AppID != "app-id" { t.Fatalf("config = %+v", config) } if api.BaseURL != sharedOneLinkAPIBaseURL { t.Fatalf("business base URL = %q, want %q", api.BaseURL, sharedOneLinkAPIBaseURL) } }) t.Run("honours template api.baseUrl", func(t *testing.T) { const custom = "https://custom.example/v5/" config, api, err := parseSharedOneLinkConfig(context.Background(), newConfig( `{"api":{"baseUrl":"`+custom+`"},"token":{"provider":"yunze","baseUrl":"http://apiv3.example.com","path":"/route/open/sharedToken"}}`, )) if err != nil { t.Fatalf("parse config: %v", err) } if config.Provider != "yunze" || config.AppID != "app-id" { t.Fatalf("config = %+v", config) } if api.BaseURL != custom { t.Fatalf("business base URL = %q, want %q", api.BaseURL, custom) } }) } // The yunze channel template is registered by its concrete temp_key, matching // the other shared-token templates (sim_onelink__v1). func TestYunzeTemplateRegistration(t *testing.T) { template, ok := service.UpstreamAPI().GetTemp(sharedOneLinkYunzeTemplateKey) if !ok || template == nil { t.Fatal("yunze template not registered") } config := &model.SimChannelConfigItemCache{ Template: &entity.TSimChannelTemplate{ TempKey: sharedOneLinkYunzeTemplateKey, TempModel: templateModelSharedToken, TempConfig: `{"token":{"provider":"yunze","baseUrl":"http://apiv3.example.com","path":"/route/open/sharedToken"}}`, }, Channel: &entity.TSimChannels{ ChanKey: "249", AppKey: "TEST_APP_KEY_0001", ChanConfig: "{}", }, } if instance := template.NewTemp(context.Background(), config); instance == nil { t.Fatal("yunze template failed to instantiate") } }