feat(server): 商业化支付与订单系统 + Harness 改进
Build and Publish Server / build (push) Successful in 4m30s

- 新增商业化模块:Stripe/Binance Pay 支付、邮箱登录、订单管理
- 新增数据模型:ReportProduct, ReportEntitlement, ReportSnapshot, PaymentEvent, EmailLoginCode
- 新增 API:/api/commerce/checkout, /api/commerce/webhooks/*, /api/commerce/orders/*
- 新增管理后台:订单列表、统计图表、统一 AdminHeader 组件
- 新增中间件测试:RateLimit, Auth, CORS 共 7 个测试用例
- 新增 AGENTS.md 项目代理指令文件
- 新增 REMINDER.md 商业化功能开发规范
- 修复 JWT 解析:添加 WithValidMethods 限制 HS256
- 修复 GetPayStatus:增加用户权限校验
- 更新 .gitignore 和 README.md
This commit is contained in:
gouki
2026-09-19 18:57:23 +00:00
parent 241a04496b
commit deb32f8fab
52 changed files with 5419 additions and 339 deletions
@@ -0,0 +1,152 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestRateLimitAllowsUnderLimit(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RateLimit())
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
for i := 0; i < 10; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.RemoteAddr = "192.168.1.1:12345"
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("request %d should be allowed, got %d", i+1, resp.Code)
}
}
}
func TestRateLimitBlocksOverLimit(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RateLimit())
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
// 发送 11 个请求,第 11 个应被限流
for i := 0; i < 11; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.RemoteAddr = "192.168.1.2:12345"
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if i < 10 && resp.Code != http.StatusOK {
t.Fatalf("request %d should be allowed, got %d", i+1, resp.Code)
}
if i == 10 && resp.Code != http.StatusTooManyRequests {
t.Fatalf("request 11 should be rate limited, got %d", resp.Code)
}
}
}
func TestRateLimitWindowReset(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RateLimit())
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
// 先发送 10 个请求达到上限
for i := 0; i < 10; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.RemoteAddr = "192.168.1.3:12345"
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
}
// 模拟时间过去 61 秒(超过 1 分钟窗口)
// 注意:由于实现使用真实时间,这里只能验证当前窗口内的行为
// 实际窗口重置测试需要 mock 时间或等待
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.RemoteAddr = "192.168.1.3:12345"
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusTooManyRequests {
t.Fatalf("should still be rate limited within window, got %d", resp.Code)
}
}
func TestAuthRejectsMissingToken(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(Auth())
router.GET("/protected", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", resp.Code)
}
}
func TestAuthRejectsMalformedToken(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(Auth())
router.GET("/protected", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
req.Header.Set("Authorization", "InvalidFormat token123")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", resp.Code)
}
}
func TestCORSAllowsConfiguredOrigin(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("CORS_ORIGINS", "https://example.com,https://app.example.com")
router := gin.New()
router.Use(CORS())
router.GET("/api/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Origin", "https://example.com")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.Code)
}
if resp.Header().Get("Access-Control-Allow-Origin") != "https://example.com" {
t.Fatalf("expected CORS header for allowed origin, got %s", resp.Header().Get("Access-Control-Allow-Origin"))
}
}
func TestCORSRejectsUnconfiguredOrigin(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("CORS_ORIGINS", "https://example.com")
router := gin.New()
router.Use(CORS())
router.GET("/api/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Origin", "https://evil.com")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
// 非白名单源不应返回 CORS 头
if resp.Header().Get("Access-Control-Allow-Origin") == "https://evil.com" {
t.Fatal("should not allow unconfigured origin")
}
}