181 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package luckygift_test
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"hyapp-luck-gateway/internal/config"
"hyapp-luck-gateway/internal/integration/luckygiftclient"
"hyapp-luck-gateway/internal/modules/luckygift"
"hyapp-luck-gateway/internal/router"
)
type fakeClient struct {
got luckygiftclient.SendCommand
calls int
}
func (f *fakeClient) SendLuckyGift(_ context.Context, cmd luckygiftclient.SendCommand) (luckygiftclient.SendResult, error) {
f.calls++
f.got = cmd
return luckygiftclient.SendResult{DrawID: "draw-1", RequestID: cmd.RequestID, AppCode: cmd.AppCode, RewardStatus: "granted"}, nil
}
func TestSendAcceptsAllowedAppAndComputesAmount(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan,yumi")
body := []byte(`{"app_code":"Aslan","request_id":"req-1","external_user_id":"u-1001","device_id":"device-attested-by-aslan","paid_at_ms":1700000000000,"gift_count":3,"unit_amount":7,"currency":"COIN","metadata":{"gift":"rose"}}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if client.calls != 1 {
t.Fatalf("client calls=%d", client.calls)
}
if client.got.AppCode != "aslan" || client.got.RequestID != "req-1" || client.got.DeviceID != "device-attested-by-aslan" || client.got.PaidAtMS != 1700000000000 || client.got.TotalAmount != 21 || client.got.GiftCount != 3 {
t.Fatalf("unexpected command: %+v", client.got)
}
}
func TestSendKeepsMissingDeviceCompatibleForFixedV2OwnerDecision(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan")
// luck-gateway 不读规则版本,因此旧 fixed_v2 调用方可继续缺省 device_id
// 如果本 App 已发布 dynamic_v3lucky-gift owner 会在规则事务内明确拒绝。
body := []byte(`{"app_code":"aslan","request_id":"req-fixed-legacy","external_user_id":"u-legacy","gift_count":1,"unit_amount":7}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || client.calls != 1 || client.got.DeviceID != "" {
t.Fatalf("fixed-compatible request status=%d calls=%d command=%+v body=%s", rec.Code, client.calls, client.got, rec.Body.String())
}
}
func TestSendRejectsPoolOverride(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan")
body := []byte(`{"app_code":"aslan","request_id":"req-1","external_user_id":"u-1001","pool_id":"pool-1","paid_at_ms":1700000000000,"gift_count":1,"unit_amount":7}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if client.calls != 0 {
t.Fatalf("client should not be called, calls=%d", client.calls)
}
}
func TestSendRejectsNegativePaidAt(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan")
body := []byte(`{"app_code":"aslan","request_id":"req-negative-paid-at","external_user_id":"u-1001","paid_at_ms":-1,"gift_count":1,"unit_amount":7}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest || client.calls != 0 {
t.Fatalf("negative paid_at_ms status=%d calls=%d body=%s", rec.Code, client.calls, rec.Body.String())
}
}
func TestSendRejectsDisallowedAppBeforeClientCall(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan,yumi")
body := []byte(`{"app_code":"hyapp","request_id":"req-1","external_user_id":"u-1001","gift_count":1,"unit_amount":9}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if client.calls != 0 {
t.Fatalf("client should not be called, calls=%d", client.calls)
}
}
func TestSendRejectsInvalidAmount(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan")
body, _ := json.Marshal(map[string]any{
"app_code": "aslan",
"request_id": "req-1",
"external_user_id": "u-1",
"gift_count": 0,
"unit_amount": 10,
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if client.calls != 0 {
t.Fatalf("client should not be called, calls=%d", client.calls)
}
}
func TestSendRejectsMoreThan999SequentialDraws(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan")
body := []byte(`{"app_code":"aslan","request_id":"req-1","external_user_id":"u-1","gift_count":1000,"unit_amount":1}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest || client.calls != 0 {
t.Fatalf("oversized sequential draw status=%d calls=%d body=%s", rec.Code, client.calls, rec.Body.String())
}
}
func TestSendRejectsTooLongExternalFields(t *testing.T) {
client := &fakeClient{}
handler := testRouter(client, "aslan")
body, _ := json.Marshal(map[string]any{
"app_code": "aslan",
"request_id": "req-1",
"external_user_id": strings.Repeat("u", 129),
"gift_count": 1,
"unit_amount": 10,
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if client.calls != 0 {
t.Fatalf("client should not be called, calls=%d", client.calls)
}
}
func testRouter(client *fakeClient, allowedApps string) http.Handler {
cfg := config.Config{AllowedApps: parseAllowedAppsForTest(allowedApps)}
return router.New(cfg, router.Handlers{LuckyGift: luckygift.New(client, time.Second, cfg.AllowedApps)})
}
func parseAllowedAppsForTest(raw string) map[string]struct{} {
out := map[string]struct{}{}
for _, app := range strings.Split(raw, ",") {
app = strings.ToLower(strings.TrimSpace(app))
if app != "" {
out[app] = struct{}{}
}
}
return out
}