feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
@@ -60,6 +61,9 @@ func doJSON(t *testing.T, r http.Handler, method, path string, body any, deviceK
|
||||
if deviceKey != "" {
|
||||
req.Header.Set("X-Device-Key", deviceKey)
|
||||
}
|
||||
if testBearer != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+testBearer)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code >= 500 {
|
||||
@@ -79,6 +83,25 @@ func doJSON(t *testing.T, r http.Handler, method, path string, body any, deviceK
|
||||
return env, key
|
||||
}
|
||||
|
||||
var testBearer string
|
||||
|
||||
func mustRegister(t *testing.T, r http.Handler) string {
|
||||
t.Helper()
|
||||
testBearer = ""
|
||||
phone := fmt.Sprintf("1%010d", time.Now().UnixNano()%10_000_000_000)
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/auth/register", map[string]any{
|
||||
"phone": phone, "password": "secret12", "nickname": "测",
|
||||
}, "")
|
||||
sess := decodeData[map[string]any](t, env.Data)
|
||||
tok, _ := sess["token"].(string)
|
||||
if tok == "" {
|
||||
t.Fatal("missing token from register")
|
||||
}
|
||||
testBearer = tok
|
||||
t.Cleanup(func() { testBearer = "" })
|
||||
return key
|
||||
}
|
||||
|
||||
func decodeData[T any](t *testing.T, raw json.RawMessage) T {
|
||||
t.Helper()
|
||||
var v T
|
||||
@@ -91,7 +114,7 @@ func decodeData[T any](t *testing.T, raw json.RawMessage) T {
|
||||
// Flow 1: create profile → portrait → deep_access mock → detail visible
|
||||
func TestFlowPortraitDeepAccess(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
|
||||
"relation": "self", "birth_date": "1990-05-12", "display_name": "我",
|
||||
@@ -136,7 +159,7 @@ func TestFlowPortraitDeepAccess(t *testing.T) {
|
||||
// Flow 2: two profiles → relation insight → deep_access → tips visible
|
||||
func TestFlowRelationDeepAccess(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
|
||||
"relation": "self", "birth_date": "1988-03-01", "display_name": "我",
|
||||
@@ -189,7 +212,7 @@ func TestFlowRelationDeepAccess(t *testing.T) {
|
||||
// Flow 3: membership mock → entitlement → portrait detail without per-report deep_access
|
||||
func TestFlowMembershipUnlock(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
|
||||
"relation": "self", "birth_date": "1995-11-07", "display_name": "我",
|
||||
@@ -232,7 +255,7 @@ func TestFlowMembershipUnlock(t *testing.T) {
|
||||
// Flow 5: profile update + soft
|
||||
func TestFlowProfileUpdateDelete(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
|
||||
"relation": "other", "birth_date": "1993-04-04", "display_name": "旧名", "relation_type": "friend",
|
||||
@@ -262,7 +285,7 @@ func TestFlowProfileUpdateDelete(t *testing.T) {
|
||||
// Flow 4: profile → ask thread → message → assistant reply + quota
|
||||
func TestFlowAskThread(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
|
||||
"relation": "self", "birth_date": "1991-02-14", "display_name": "我",
|
||||
@@ -308,3 +331,56 @@ func TestFlowAskThread(t *testing.T) {
|
||||
t.Fatalf("expected user+assistant history, got %d", len(items))
|
||||
}
|
||||
}
|
||||
|
||||
// Flow 4b: exhaust free ask quota → buy ask_pack → can ask again
|
||||
func TestFlowAskPackPurchase(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/profiles", map[string]any{
|
||||
"relation": "self", "birth_date": "1991-02-14", "display_name": "我",
|
||||
}, key)
|
||||
profileID := decodeData[map[string]any](t, env.Data)["id"].(string)
|
||||
|
||||
env, key = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads", map[string]any{
|
||||
"profile_id": profileID, "scene": "self",
|
||||
}, key)
|
||||
threadID := decodeData[map[string]any](t, env.Data)["id"].(string)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
_, key = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
|
||||
"content": "第几问",
|
||||
}, key)
|
||||
}
|
||||
|
||||
env, key, status := doJSONExpect(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
|
||||
"content": "应该没额度了",
|
||||
}, key, 40210)
|
||||
if status != http.StatusPaymentRequired {
|
||||
t.Fatalf("expected 402 after free exhaust, got %d %#v", status, env)
|
||||
}
|
||||
|
||||
env, key = doJSON(t, r, http.MethodPost, "/api/v1/orders", map[string]any{
|
||||
"kind": "ask_pack", "plan": "pack10",
|
||||
}, key)
|
||||
orderID := decodeData[map[string]any](t, env.Data)["order_id"].(string)
|
||||
_, key = doJSON(t, r, http.MethodPost, "/api/v1/orders/"+orderID+"/pay-mock", nil, key)
|
||||
|
||||
env, key = doJSON(t, r, http.MethodGet, "/api/v1/ask/quota", nil, key)
|
||||
q := decodeData[map[string]any](t, env.Data)
|
||||
rem, _ := q["remaining"].(float64)
|
||||
paid, _ := q["paid_left"].(float64)
|
||||
if rem < 10 || paid < 10 {
|
||||
t.Fatalf("expected paid pack quota, got %#v", q)
|
||||
}
|
||||
|
||||
env, _ = doJSON(t, r, http.MethodPost, "/api/v1/ask/threads/"+threadID+"/messages", map[string]any{
|
||||
"content": "买完额度继续问",
|
||||
}, key)
|
||||
out := decodeData[map[string]any](t, env.Data)
|
||||
q2, _ := out["quota"].(map[string]any)
|
||||
rem2, _ := q2["remaining"].(float64)
|
||||
if rem2 != rem-1 {
|
||||
t.Fatalf("paid quota should decrease: before=%v after=%v", rem, rem2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user