sync: 同步生产版 v1.0.4 并脱敏
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/upstreamapi"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
)
|
||||
|
||||
func TestAPNProxyHTTPFailureIsUncertainOnlyOnAPNRoute(t *testing.T) {
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
defer g.Cfg().SetAdapter(oldAdapter)
|
||||
for _, tc := range []struct {
|
||||
name, path, body string
|
||||
disconnect, uncertain bool
|
||||
}{
|
||||
{"HTTP200顶层9999", "/sim/change/sim-apn", `{"code":9999,"message":"模拟网关未确认"}`, false, true},
|
||||
{"通道连接断开", "/sim/change/sim-apn", "", true, true},
|
||||
{"非APN同名字段", "/sim/change/sim-status", `{"code":9999,"message":"模拟非APN网关错误"}`, false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
chanServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if tc.disconnect {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(tc.body))
|
||||
}))
|
||||
defer chanServer.Close()
|
||||
adapter, err := gcfg.NewAdapterContent(`{"gateway":{"address":"` + strings.TrimPrefix(chanServer.URL, "http://") + `","timeout":"1s"}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
api := upstreamapi.NewUpstreamAPI()
|
||||
api.RegisterChan("fake-channel", "test", &model.SimCardChannelCache{Instance: &sSimYD{}})
|
||||
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var input model.SimBaseReqJson
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
ctx := (&ghttp.Request{Request: r}).Context()
|
||||
items, err := api.SimBase(ctx, &input)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": items})
|
||||
}))
|
||||
defer proxy.Close()
|
||||
body := []byte(`{"simKeys":[{"chanKey":"fake-channel","iccid":"fake-card"}],"reqData":{"operationId":"fake-operation","submitOnly":true}}`)
|
||||
response, err := http.Post(proxy.URL+tc.path, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var output struct {
|
||||
Code int `json:"code"`
|
||||
Data model.SimBaseResJson `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(response.Body).Decode(&output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != 200 || output.Code != 0 || len(output.Data) != 1 || output.Data[0].Code != 9999 || output.Data[0].Message == "" {
|
||||
t.Fatalf("代理异常响应无效:HTTP=%d result=%+v", response.StatusCode, output)
|
||||
}
|
||||
isUncertain := strings.Contains(string(output.Data[0].ResData), `"uncertain":true`)
|
||||
if isUncertain != tc.uncertain {
|
||||
t.Fatalf("APN路由隔离失效:%s", output.Data[0].ResData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPNProxyHTTPKeepsDurableJobAndFrozenRequest(t *testing.T) {
|
||||
oldAdapter := g.Cfg().GetAdapter()
|
||||
defer g.Cfg().SetAdapter(oldAdapter)
|
||||
var captured model.SimBaseReqJson
|
||||
chanServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":[{"code":0,"simKey":{"chanKey":"fake-channel","iccid":"fake-card","msisdn":"frozen-number"},"resData":{"orderNum":"saved-job","pending":true}}]}`))
|
||||
}))
|
||||
defer chanServer.Close()
|
||||
adapter, err := gcfg.NewAdapterContent(`{"gateway":{"address":"` + strings.TrimPrefix(chanServer.URL, "http://") + `","timeout":"1s"}}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
api := upstreamapi.NewUpstreamAPI()
|
||||
api.RegisterChan("fake-channel", "test", &model.SimCardChannelCache{Instance: &sSimYD{}})
|
||||
input := &model.SimBaseReqJson{SimKeys: []*model.SimKey{{ChanKey: "fake-channel", ICCID: "fake-card", MSISDN: "frozen-number"}}, ReqData: json.RawMessage(`{"operationId":"stable-operation","recoverOnly":true}`)}
|
||||
ctx := (&ghttp.Request{Request: httptest.NewRequest("POST", "/sim/change/sim-apn", nil)}).Context()
|
||||
items, err := api.SimBase(ctx, input)
|
||||
if err != nil || len(items) != 1 || !strings.Contains(string(items[0].ResData), `"orderNum":"saved-job"`) || !strings.Contains(string(items[0].ResData), `"pending":true`) {
|
||||
t.Fatalf("HTTP转发丢失jobId:%+v %v", items, err)
|
||||
}
|
||||
if string(captured.ReqData) != string(input.ReqData) || captured.SimKeys[0].MSISDN != "frozen-number" {
|
||||
t.Fatalf("恢复请求被改写:%+v", captured)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,80 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"strings"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/gclient"
|
||||
)
|
||||
|
||||
const (
|
||||
// gatewayBatchTimeoutConfigKey 限制单次批量转发的最长等待,防止上游异常时无限占用连接。
|
||||
gatewayBatchTimeoutConfigKey = "gateway.batchTimeout"
|
||||
// defaultGatewayBatchTimeout 覆盖 100 张卡按单卡 10 秒预算的最坏耗时,并留有余量。
|
||||
defaultGatewayBatchTimeout = 20 * time.Minute
|
||||
// defaultDataServiceTimeout 是数据开停转发的独立预算。
|
||||
defaultDataServiceTimeout = 210 * time.Second
|
||||
)
|
||||
|
||||
// configureGatewayTimeout 让响应头等待与整次请求使用同一预算,避免默认30秒提前中断。
|
||||
// upstream-chan 对批量操作按卡串行调用运营商接口,耗时随卡数线性增长;
|
||||
// 因此批量请求不能沿用 gclient 默认的 30 秒响应头超时,否则请求仍在办理就会被判为失败。
|
||||
func configureGatewayTimeout(ctx context.Context, client *gclient.Client, path string, count int, perCard time.Duration) {
|
||||
if path == "/sim/change/sim-apn" || path == "/sim/query/sim-apn-capability" {
|
||||
client.SetContentType("application/json")
|
||||
}
|
||||
applyGatewayTimeout(client, path, count, perCard,
|
||||
configDuration(ctx, "gateway.dataServiceTimeout", defaultDataServiceTimeout),
|
||||
configDuration(ctx, gatewayBatchTimeoutConfigKey, defaultGatewayBatchTimeout))
|
||||
}
|
||||
|
||||
// applyGatewayTimeout 计算并应用单次转发预算:整次请求与响应头等待必须一致,
|
||||
// 否则响应头超时会在业务仍在办理时提前中断请求。
|
||||
func applyGatewayTimeout(client *gclient.Client, path string, count int, perCard, dataServiceTimeout, batchTimeout time.Duration) {
|
||||
if count < 1 {
|
||||
count = 1
|
||||
}
|
||||
if perCard <= 0 {
|
||||
perCard = 10 * time.Second
|
||||
}
|
||||
timeout := perCard * time.Duration(count)
|
||||
if path == "/sim/change/sim-apn" {
|
||||
timeout = dataServiceTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultDataServiceTimeout
|
||||
}
|
||||
} else if batchTimeout > 0 && timeout > batchTimeout {
|
||||
timeout = batchTimeout
|
||||
}
|
||||
client.SetTimeout(timeout)
|
||||
if transport, ok := client.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// configDuration 读取时长配置,兼容写成 "20m" 字符串或秒数整数两种形式。
|
||||
func configDuration(ctx context.Context, key string, fallback time.Duration) time.Duration {
|
||||
value := g.Cfg().MustGet(ctx, key)
|
||||
if value == nil || value.IsNil() {
|
||||
return fallback
|
||||
}
|
||||
if duration := value.Duration(); duration >= time.Second {
|
||||
return duration
|
||||
}
|
||||
if seconds := value.Int(); seconds > 0 {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func readGatewayResponse(response *gclient.Response) (model.SimBaseResJson, error) {
|
||||
defer response.Close()
|
||||
body := response.ReadAll()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package upstreamimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func TestGatewayTimeoutAllowsDelayedHeaders(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
_, _ = w.Write([]byte(`{"code":0,"data":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
client := g.Client()
|
||||
other := g.Client()
|
||||
original := other.Transport.(*http.Transport)
|
||||
original.ResponseHeaderTimeout = time.Millisecond
|
||||
client.Transport.(*http.Transport).ResponseHeaderTimeout = time.Millisecond
|
||||
configureGatewayTimeout(context.Background(), client, "/sim/change/sim-apn", 1, time.Second)
|
||||
response, err := client.Post(context.Background(), server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Close()
|
||||
if original.ResponseHeaderTimeout != time.Millisecond {
|
||||
t.Fatal("修改了共享Transport")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonAPNGatewayTimeoutPreservesHeaderBudget(t *testing.T) {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
transport.ResponseHeaderTimeout = 17 * time.Second
|
||||
configureGatewayTimeout(context.Background(), client, "/sim/query/test", 2, 3*time.Second)
|
||||
if client.Client.Timeout != 6*time.Second || transport.ResponseHeaderTimeout != 6*time.Second {
|
||||
t.Fatalf("非APN响应头预算必须与请求预算一致:request=%s header=%s", client.Client.Timeout, transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonAPNBatchExpandsHeaderBudget 覆盖批量停复机等非 APN 路径:
|
||||
// 50 张卡按单卡预算放大后,响应头等待不能继续停留在 gclient 的 30 秒默认值。
|
||||
func TestNonAPNBatchExpandsHeaderBudget(t *testing.T) {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
transport.ResponseHeaderTimeout = 30 * time.Second
|
||||
applyGatewayTimeout(client, "/sim/change/sim-status", 50, 10*time.Second, defaultDataServiceTimeout, defaultGatewayBatchTimeout)
|
||||
if client.Client.Timeout != 500*time.Second {
|
||||
t.Fatalf("请求预算=%s,期望500s", client.Client.Timeout)
|
||||
}
|
||||
if transport.ResponseHeaderTimeout != client.Client.Timeout {
|
||||
t.Fatalf("响应头预算=%s与请求预算不一致", transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonAPNBatchRespectsBatchTimeoutCap 保证异常上游不会无限占用连接:
|
||||
// 超出批量上限时按上限截断,且响应头预算同步截断。
|
||||
func TestNonAPNBatchRespectsBatchTimeoutCap(t *testing.T) {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
applyGatewayTimeout(client, "/sim/query/usage", 1000, 10*time.Second, defaultDataServiceTimeout, defaultGatewayBatchTimeout)
|
||||
if client.Client.Timeout != defaultGatewayBatchTimeout {
|
||||
t.Fatalf("请求预算=%s,期望批量上限%s", client.Client.Timeout, defaultGatewayBatchTimeout)
|
||||
}
|
||||
if transport.ResponseHeaderTimeout != defaultGatewayBatchTimeout {
|
||||
t.Fatalf("响应头预算=%s,期望批量上限%s", transport.ResponseHeaderTimeout, defaultGatewayBatchTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonAPNSingleCardKeepsPerCardBudget 保证单卡请求不被放大成批量预算。
|
||||
func TestNonAPNSingleCardKeepsPerCardBudget(t *testing.T) {
|
||||
for _, count := range []int{0, 1} {
|
||||
client := g.Client()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
applyGatewayTimeout(client, "/sim/query/test", count, 10*time.Second, defaultDataServiceTimeout, defaultGatewayBatchTimeout)
|
||||
if client.Client.Timeout != 10*time.Second || transport.ResponseHeaderTimeout != 10*time.Second {
|
||||
t.Fatalf("count=%d request=%s header=%s", count, client.Client.Timeout, transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataServiceTimeoutOverridesCardBudget(t *testing.T) {
|
||||
for _, count := range []int{0, 1, 100} {
|
||||
client := g.Client()
|
||||
configureGatewayTimeout(context.Background(), client, "/sim/change/sim-apn", count, time.Millisecond)
|
||||
if client.Client.Timeout != 210*time.Second {
|
||||
t.Fatalf("count=%d timeout=%s", count, client.Client.Timeout)
|
||||
}
|
||||
if client.Transport.(*http.Transport).ResponseHeaderTimeout != client.Client.Timeout {
|
||||
t.Fatal("响应头预算不一致")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
_ "git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
)
|
||||
|
||||
func TestMingqianProxyTemplate(t *testing.T) {
|
||||
|
||||
@@ -3,11 +3,11 @@ package upstreamimpl
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/logic/upstreamimpl/dx"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
@@ -85,7 +85,7 @@ func (s *sSimDX) SimBase(ctx context.Context, input *model.SimBaseReqJson) (mode
|
||||
}
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
client.SetTimeout(s.getUpstreamTimeout(ctx) * time.Duration(len(input.SimKeys)))
|
||||
configureGatewayTimeout(ctx, client, path, len(input.SimKeys), s.getUpstreamTimeout(ctx))
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "SimBase post error %+v,url %s, params %+v, ", err, url, input)
|
||||
|
||||
@@ -3,10 +3,10 @@ package upstreamimpl
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-upstream-proxy/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
@@ -106,11 +106,11 @@ func (s *sSimYD) SimBase(ctx context.Context, input *model.SimBaseReqJson) (mode
|
||||
g.Log().Debugf(ctx, "SimYD.SimBase, url %s", url)
|
||||
client := g.Client()
|
||||
client.SetDiscovery(gsvc.GetRegistry())
|
||||
client.SetTimeout(s.getUpstreamTimeout(ctx) * time.Duration(len(input.SimKeys)))
|
||||
configureGatewayTimeout(ctx, client, path, len(input.SimKeys), s.getUpstreamTimeout(ctx))
|
||||
resp, err := client.Post(ctx, url, input)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "SimYD.SimBase, post error %+v,url %s, params %+v, ", err, url, input)
|
||||
return nil, gerror.New("请求失败")
|
||||
return nil, gerror.Wrap(err, "请求通道失败")
|
||||
}
|
||||
return readGatewayResponse(resp)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user