实现多商户履约平台基础
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
"affiliate_dash/internal/service"
|
||||
"affiliate_dash/internal/testdb"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func newOpenAuthTestServer(t *testing.T, signatureVersion string) (*gin.Engine, string, string) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := testdb.New(t, &model.Merchant{}, &model.APIClient{}, &model.APIRequestNonce{})
|
||||
codec, err := service.NewSecretCodec("test-master-key")
|
||||
if err != nil {
|
||||
t.Fatalf("codec: %v", err)
|
||||
}
|
||||
merchant := model.Merchant{Code: "merchant-open", Name: "开放测试商户", Status: model.MerchantStatusActive}
|
||||
if err := db.Create(&merchant).Error; err != nil {
|
||||
t.Fatalf("create merchant: %v", err)
|
||||
}
|
||||
secret := "client-secret"
|
||||
ciphertext, err := codec.Encrypt(secret)
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt secret: %v", err)
|
||||
}
|
||||
client := model.APIClient{
|
||||
MerchantID: merchant.ID,
|
||||
Name: "测试客户端",
|
||||
AppKey: "ak_test",
|
||||
SecretCiphertext: ciphertext,
|
||||
SignatureVersion: signatureVersion,
|
||||
Scopes: "*",
|
||||
Status: model.APIClientStatusActive,
|
||||
}
|
||||
if err := db.Create(&client).Error; err != nil {
|
||||
t.Fatalf("create client: %v", err)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
r.Use(OpenAuth(OpenAuthConfig{
|
||||
DB: db,
|
||||
Codec: codec,
|
||||
SkewSeconds: 300,
|
||||
}))
|
||||
r.POST("/api/client/v1/orders", RequireAPIScope("orders:write"), func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"merchant_id": GetMerchantID(c),
|
||||
"client_id": GetAPIClient(c).ID,
|
||||
})
|
||||
})
|
||||
return r, client.AppKey, secret
|
||||
}
|
||||
|
||||
func TestOpenAuthV1AcceptsSignedRequestAndRejectsReplay(t *testing.T) {
|
||||
r, appKey, secret := newOpenAuthTestServer(t, "v1")
|
||||
body := `{"client_order_no":"client-001","sku":"sku-basic"}`
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
nonce := "nonce-123456"
|
||||
path := "/api/client/v1/orders"
|
||||
sign := BuildOpenV1Sign(secret, ts, nonce, http.MethodPost, path, []byte(body))
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
||||
req.Header.Set("X-App-Key", appKey)
|
||||
req.Header.Set("X-Timestamp", ts)
|
||||
req.Header.Set("X-Nonce", nonce)
|
||||
req.Header.Set("X-Sign", sign)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("signed request should pass, code=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
replay := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
||||
replay.Header = req.Header.Clone()
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, replay)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("replay should be rejected, code=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAuthRejectsSignatureMismatch(t *testing.T) {
|
||||
r, appKey, _ := newOpenAuthTestServer(t, "v1")
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/client/v1/orders", strings.NewReader(`{"a":1}`))
|
||||
req.Header.Set("X-App-Key", appKey)
|
||||
req.Header.Set("X-Timestamp", strconv.FormatInt(time.Now().Unix(), 10))
|
||||
req.Header.Set("X-Nonce", "nonce-bad-sign")
|
||||
req.Header.Set("X-Sign", "bad-sign")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("bad signature should be rejected, code=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceOpenAuthKeepsLegacyUpstreamSignature(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
appKey := "source-key"
|
||||
secret := "source-secret"
|
||||
r := gin.New()
|
||||
r.Use(SourceOpenAuth(SourceOpenAuthConfig{
|
||||
APIKey: appKey,
|
||||
APISecret: secret,
|
||||
SkewSeconds: 300,
|
||||
}))
|
||||
r.POST("/api/open/v1/orders/ship-notify", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
body := `{"client_order_no":"client-legacy","sku":"sku-basic"}`
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
nonce := "legacy-nonce-123"
|
||||
path := "/api/open/v1/orders/ship-notify"
|
||||
sign := BuildOpenSign(appKey, secret, ts, nonce, http.MethodPost, path, body)
|
||||
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
||||
req.Header.Set("X-Api-Key", appKey)
|
||||
req.Header.Set("X-Timestamp", ts)
|
||||
req.Header.Set("X-Nonce", nonce)
|
||||
req.Header.Set("X-Sign", sign)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("legacy signed request should pass, code=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceOpenAuthAcceptsUppercaseLegacySignature(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
appKey := "source-key"
|
||||
secret := "source-secret"
|
||||
r := gin.New()
|
||||
r.Use(SourceOpenAuth(SourceOpenAuthConfig{
|
||||
APIKey: appKey,
|
||||
APISecret: secret,
|
||||
SkewSeconds: 300,
|
||||
}))
|
||||
r.GET("/api/open/v1/orders/O123", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
nonce := "legacy-nonce-upper"
|
||||
path := "/api/open/v1/orders/O123"
|
||||
sign := strings.ToUpper(BuildOpenSign(appKey, secret, ts, nonce, http.MethodGet, path, ""))
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
req.Header.Set("X-Api-Key", appKey)
|
||||
req.Header.Set("X-Timestamp", ts)
|
||||
req.Header.Set("X-Nonce", nonce)
|
||||
req.Header.Set("X-Sign", sign)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("uppercase legacy signature should pass, code=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user