Files

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())
}
}