package handler import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/middleware" "github.com/gouki/lunar-server/internal/model" "github.com/gouki/lunar-server/internal/service" ) func TestCommerceCheckoutRequiresAuthentication(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/commerce/checkout", middleware.Auth(), CreateReportCheckout) request := httptest.NewRequest(http.MethodPost, "/api/commerce/checkout", strings.NewReader(`{"provider":"stripe","reportId":"ZM-ABCDEFG","snapshot":{"version":1,"result":"test"}}`)) request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d: %s", response.Code, response.Body.String()) } if !strings.Contains(response.Body.String(), `"code":401`) { t.Fatalf("unexpected response body: %s", response.Body.String()) } } func TestStripeWebhookRejectsUnsignedRequest(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/commerce/webhooks/stripe", StripeWebhook) request := httptest.NewRequest(http.MethodPost, "/api/commerce/webhooks/stripe", strings.NewReader(`{"id":"evt_invalid"}`)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", response.Code) } } func TestEmailLoginRejectsMalformedBody(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.POST("/api/auth/email/request", RequestEmailLoginCode) request := httptest.NewRequest(http.MethodPost, "/api/auth/email/request", strings.NewReader(`{}`)) request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", response.Code) } if !strings.Contains(response.Body.String(), `"code":400`) { t.Fatalf("unexpected response body: %s", response.Body.String()) } } func TestStripeCheckoutWebhookAndEntitlementHTTPFlow(t *testing.T) { gin.SetMode(gin.TestMode) stripeAPI := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") fmt.Fprint(response, `{"id":"cs_test_flow","url":"https://checkout.stripe.test/flow"}`) })) defer stripeAPI.Close() t.Setenv("DB_TYPE", "sqlite") t.Setenv("DB_NAME", "file:commerce-http-test?mode=memory&cache=shared") t.Setenv("JWT_SECRET", "http-test-secret") t.Setenv("STRIPE_SECRET_KEY", "sk_test") t.Setenv("STRIPE_WEBHOOK_SECRET", "whsec_http_test") t.Setenv("STRIPE_API_BASE_URL", stripeAPI.URL) t.Setenv("WEB_APP_URL", "https://example.test") if err := config.InitDB(config.Load()); err != nil { t.Fatal(err) } user := model.User{OpenID: "email:http-test", Email: "buyer@example.com", Nickname: "buyer", Status: 1} if err := config.GetDB().Create(&user).Error; err != nil { t.Fatal(err) } token, err := service.NewUserService().GenerateToken(user.ID, "http-test-secret") if err != nil { t.Fatal(err) } router := gin.New() router.POST("/api/commerce/checkout", middleware.Auth(), CreateReportCheckout) router.POST("/api/commerce/webhooks/stripe", StripeWebhook) router.GET("/api/commerce/orders/:orderNo", middleware.Auth(), GetCommerceOrder) router.GET("/api/commerce/reports/:reportId/access", middleware.Auth(), GetReportAccess) router.GET("/api/commerce/reports/recover/:accessCode", middleware.Auth(), GetReportByAccessCode) checkoutRequest := httptest.NewRequest(http.MethodPost, "/api/commerce/checkout", strings.NewReader(`{"provider":"stripe","reportId":"ZM-ABCDEFG","snapshot":{"version":1,"id":"ZM-ABCDEFG","result":{"eightChar":{"dayMaster":"甲","dayMasterElement":"木"},"decadeFortunes":[{"ganzhi":"丙寅","startAge":8}]},"elements":{"dominant":"木","weakest":"金"},"shensha":[],"sections":[]}}`)) checkoutRequest.Header.Set("Content-Type", "application/json") checkoutRequest.Header.Set("Authorization", "Bearer "+token) checkoutResponse := httptest.NewRecorder() router.ServeHTTP(checkoutResponse, checkoutRequest) if checkoutResponse.Code != http.StatusOK { t.Fatalf("expected checkout 200, got %d: %s", checkoutResponse.Code, checkoutResponse.Body.String()) } var checkoutPayload struct { Data struct { OrderNo string `json:"orderNo"` } `json:"data"` } if err := json.Unmarshal(checkoutResponse.Body.Bytes(), &checkoutPayload); err != nil || checkoutPayload.Data.OrderNo == "" { t.Fatalf("invalid checkout response: %s", checkoutResponse.Body.String()) } now := time.Now() webhookBody := []byte(fmt.Sprintf(`{"id":"evt_http_flow","type":"checkout.session.completed","data":{"object":{"id":"cs_test_flow","payment_intent":"pi_http_flow","payment_status":"paid","amount_total":499,"currency":"usd","metadata":{"order_no":"%s"}}}}`, checkoutPayload.Data.OrderNo)) mac := hmac.New(sha256.New, []byte("whsec_http_test")) mac.Write([]byte(fmt.Sprintf("%d.%s", now.Unix(), webhookBody))) webhookRequest := httptest.NewRequest(http.MethodPost, "/api/commerce/webhooks/stripe", strings.NewReader(string(webhookBody))) webhookRequest.Header.Set("Stripe-Signature", fmt.Sprintf("t=%d,v1=%s", now.Unix(), hex.EncodeToString(mac.Sum(nil)))) webhookResponse := httptest.NewRecorder() router.ServeHTTP(webhookResponse, webhookRequest) if webhookResponse.Code != http.StatusOK { t.Fatalf("expected webhook 200, got %d: %s", webhookResponse.Code, webhookResponse.Body.String()) } accessRequest := httptest.NewRequest(http.MethodGet, "/api/commerce/reports/ZM-ABCDEFG/access", nil) accessRequest.Header.Set("Authorization", "Bearer "+token) accessResponse := httptest.NewRecorder() router.ServeHTTP(accessResponse, accessRequest) if accessResponse.Code != http.StatusOK || !strings.Contains(accessResponse.Body.String(), `"unlocked":true`) { t.Fatalf("expected active entitlement, got %d: %s", accessResponse.Code, accessResponse.Body.String()) } orderRequest := httptest.NewRequest(http.MethodGet, "/api/commerce/orders/"+checkoutPayload.Data.OrderNo, nil) orderRequest.Header.Set("Authorization", "Bearer "+token) orderResponse := httptest.NewRecorder() router.ServeHTTP(orderResponse, orderRequest) var orderPayload struct { Data struct { AccessCode string `json:"accessCode"` } `json:"data"` } if err := json.Unmarshal(orderResponse.Body.Bytes(), &orderPayload); err != nil || orderPayload.Data.AccessCode == "" { t.Fatalf("expected server recovery code, got %s", orderResponse.Body.String()) } recoverRequest := httptest.NewRequest(http.MethodGet, "/api/commerce/reports/recover/"+orderPayload.Data.AccessCode, nil) recoverRequest.Header.Set("Authorization", "Bearer "+token) recoverResponse := httptest.NewRecorder() router.ServeHTTP(recoverResponse, recoverRequest) if recoverResponse.Code != http.StatusOK || !strings.Contains(recoverResponse.Body.String(), `"title":"性格底色"`) { t.Fatalf("expected server-generated premium snapshot, got %d: %s", recoverResponse.Code, recoverResponse.Body.String()) } }