175 lines
5.5 KiB
Go
175 lines
5.5 KiB
Go
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, appKey, 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)
|
|
db := testdb.New(t)
|
|
appKey := "source-key"
|
|
secret := "source-secret"
|
|
r := gin.New()
|
|
r.Use(SourceOpenAuth(SourceOpenAuthConfig{
|
|
DB: db,
|
|
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())
|
|
}
|
|
|
|
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("legacy replay should be rejected, code=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSourceOpenAuthAcceptsUppercaseLegacySignature(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := testdb.New(t)
|
|
appKey := "source-key"
|
|
secret := "source-secret"
|
|
r := gin.New()
|
|
r.Use(SourceOpenAuth(SourceOpenAuthConfig{
|
|
DB: db,
|
|
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())
|
|
}
|
|
}
|