feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,7 +59,7 @@ func TestAdminOpsPhaseA(t *testing.T) {
|
||||
t.Fatalf("login token missing: %v %s", err, env.Data)
|
||||
}
|
||||
|
||||
_, _ = doJSON(t, r, http.MethodGet, "/api/v1/profiles", nil, "")
|
||||
_ = mustRegister(t, r)
|
||||
|
||||
env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users", nil, login.Token)
|
||||
if code != 200 || env.Code != 0 {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAnalyticsOpsB(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
sid := "s_test_" + time.Now().Format("150405")
|
||||
now := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/analytics/events", map[string]any{
|
||||
"items": []map[string]any{
|
||||
{"name": "session_start", "session_id": sid, "client_ts": now, "props": map[string]any{"cold": true}},
|
||||
{"name": "page_view", "session_id": sid, "page_path": "/portrait", "client_ts": now},
|
||||
{"name": "page_view", "session_id": sid, "page_path": "/ask", "client_ts": now},
|
||||
{"name": "page_leave", "session_id": sid, "page_path": "/portrait", "client_ts": now,
|
||||
"props": map[string]any{"dwell_ms": 1500}},
|
||||
{"name": "ui_click", "session_id": sid, "client_ts": now,
|
||||
"props": map[string]any{"element_id": "home_cta", "page_path": "/"}},
|
||||
{"name": "portrait_completed", "session_id": sid, "client_ts": now},
|
||||
{"name": "session_end", "session_id": sid, "client_ts": now,
|
||||
"props": map[string]any{"exit_page": "/ask", "duration_ms": 8000}},
|
||||
},
|
||||
}, "")
|
||||
var accepted struct {
|
||||
Accepted int `json:"accepted"`
|
||||
}
|
||||
if err := json.Unmarshal(env.Data, &accepted); err != nil || accepted.Accepted < 6 {
|
||||
t.Fatalf("accepted=%v err=%v body=%s", accepted, err, string(env.Data))
|
||||
}
|
||||
_ = key
|
||||
|
||||
loginEnv, code := doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/auth/login", map[string]string{
|
||||
"username": "admin", "password": "change-me",
|
||||
}, "")
|
||||
if code != http.StatusOK || loginEnv.Code != 0 {
|
||||
t.Fatalf("admin login http=%d code=%d", code, loginEnv.Code)
|
||||
}
|
||||
var login struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
_ = json.Unmarshal(loginEnv.Data, &login)
|
||||
if login.Token == "" {
|
||||
t.Fatal("missing admin token")
|
||||
}
|
||||
|
||||
from := time.Now().UTC().Add(-24 * time.Hour).Format("2006-01-02")
|
||||
to := time.Now().UTC().Format("2006-01-02")
|
||||
q := "?from=" + from + "&to=" + to
|
||||
|
||||
ov, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/analytics/overview"+q, nil, login.Token)
|
||||
if code != http.StatusOK || ov.Code != 0 {
|
||||
t.Fatalf("overview http=%d code=%d msg=%s", code, ov.Code, ov.Message)
|
||||
}
|
||||
var overview map[string]any
|
||||
_ = json.Unmarshal(ov.Data, &overview)
|
||||
if sessions, _ := overview["sessions"].(float64); sessions < 1 {
|
||||
t.Fatalf("expected sessions>=1 got %v", overview)
|
||||
}
|
||||
|
||||
pg, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/analytics/pages"+q, nil, login.Token)
|
||||
if code != http.StatusOK || pg.Code != 0 {
|
||||
t.Fatalf("pages http=%d code=%d", code, pg.Code)
|
||||
}
|
||||
ex, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/analytics/exits"+q, nil, login.Token)
|
||||
if code != http.StatusOK || ex.Code != 0 {
|
||||
t.Fatalf("exits http=%d code=%d", code, ex.Code)
|
||||
}
|
||||
var exits struct {
|
||||
Items []struct {
|
||||
ExitPage string `json:"exit_page"`
|
||||
Count int `json:"count"`
|
||||
} `json:"items"`
|
||||
}
|
||||
_ = json.Unmarshal(ex.Data, &exits)
|
||||
found := false
|
||||
for _, it := range exits.Items {
|
||||
if it.ExitPage == "/ask" && it.Count >= 1 {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected exit /ask in %v", exits.Items)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func TestExploreCatalog(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
env, key := doJSON(t, r, http.MethodGet, "/api/v1/explore/catalog", nil, key)
|
||||
data := decodeData[map[string]any](t, env.Data)
|
||||
cats, ok := data["categories"].([]any)
|
||||
@@ -24,7 +24,7 @@ func TestExploreCatalog(t *testing.T) {
|
||||
|
||||
func TestGrowthPlanCheckin(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/growth/plans", map[string]any{
|
||||
"title": "每晚早睡", "focus": "保护睡眠",
|
||||
}, key)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpsContentPhaseC(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
|
||||
env, _ := doJSON(t, r, http.MethodGet, "/api/v1/home/tools", nil, "")
|
||||
var home struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
}
|
||||
if err := json.Unmarshal(env.Data, &home); err != nil || len(home.Items) < 6 {
|
||||
t.Fatalf("home tools seed: %v len=%d", err, len(home.Items))
|
||||
}
|
||||
|
||||
loginEnv, code := doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/auth/login", map[string]string{
|
||||
"username": "admin", "password": "change-me",
|
||||
}, "")
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("login http=%d", code)
|
||||
}
|
||||
var login struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
_ = json.Unmarshal(loginEnv.Data, &login)
|
||||
|
||||
adminTools, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/home/tools", nil, login.Token)
|
||||
if code != http.StatusOK || adminTools.Code != 0 {
|
||||
t.Fatalf("admin tools http=%d code=%d", code, adminTools.Code)
|
||||
}
|
||||
var all struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
}
|
||||
_ = json.Unmarshal(adminTools.Data, &all)
|
||||
if len(all.Items) == 0 {
|
||||
t.Fatal("expected seeded tools")
|
||||
}
|
||||
// disable first tool
|
||||
all.Items[0]["enabled"] = false
|
||||
putEnv, code := doAdminJSON(t, r, http.MethodPut, "/api/v1/admin/home/tools", map[string]any{
|
||||
"items": all.Items,
|
||||
}, login.Token)
|
||||
if code != http.StatusOK || putEnv.Code != 0 {
|
||||
t.Fatalf("put tools http=%d code=%d msg=%s", code, putEnv.Code, putEnv.Message)
|
||||
}
|
||||
|
||||
auditEnv, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/audit-logs?limit=5", nil, login.Token)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("audit http=%d", code)
|
||||
}
|
||||
var audits struct {
|
||||
Items []struct {
|
||||
Action string `json:"action"`
|
||||
} `json:"items"`
|
||||
}
|
||||
_ = json.Unmarshal(auditEnv.Data, &audits)
|
||||
foundAudit := false
|
||||
for _, a := range audits.Items {
|
||||
if a.Action == "home_tools.replace" {
|
||||
foundAudit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundAudit {
|
||||
t.Fatalf("expected home_tools.replace audit, got %+v", audits.Items)
|
||||
}
|
||||
|
||||
env, _ = doJSON(t, r, http.MethodGet, "/api/v1/home/tools", nil, "")
|
||||
_ = json.Unmarshal(env.Data, &home)
|
||||
enabledN := len(home.Items)
|
||||
if enabledN >= len(all.Items) {
|
||||
t.Fatalf("expected fewer enabled tools after disable, pub=%d admin=%d", enabledN, len(all.Items))
|
||||
}
|
||||
|
||||
// restore all enabled for shared DB
|
||||
for i := range all.Items {
|
||||
all.Items[i]["enabled"] = true
|
||||
}
|
||||
_, _ = doAdminJSON(t, r, http.MethodPut, "/api/v1/admin/home/tools", map[string]any{"items": all.Items}, login.Token)
|
||||
|
||||
scalesEnv, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/scales", nil, login.Token)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("scales http=%d", code)
|
||||
}
|
||||
var scales struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Status string `json:"status"`
|
||||
} `json:"items"`
|
||||
}
|
||||
_ = json.Unmarshal(scalesEnv.Data, &scales)
|
||||
if len(scales.Items) == 0 {
|
||||
t.Fatal("no scales")
|
||||
}
|
||||
target := scales.Items[0]
|
||||
patch, code := doAdminJSON(t, r, http.MethodPatch, "/api/v1/admin/scales/"+target.ID, map[string]string{
|
||||
"status": "draft",
|
||||
}, login.Token)
|
||||
if code != http.StatusOK || patch.Code != 0 {
|
||||
t.Fatalf("patch http=%d code=%d msg=%s", code, patch.Code, patch.Message)
|
||||
}
|
||||
|
||||
// published list should not include draft slug — needs registered user
|
||||
key := mustRegister(t, r)
|
||||
listEnv, _ := doJSON(t, r, http.MethodGet, "/api/v1/scales", nil, key)
|
||||
var pub struct {
|
||||
Items []struct {
|
||||
Slug string `json:"slug"`
|
||||
} `json:"items"`
|
||||
}
|
||||
_ = json.Unmarshal(listEnv.Data, &pub)
|
||||
for _, it := range pub.Items {
|
||||
if it.Slug == target.Slug {
|
||||
t.Fatalf("draft scale %s still in published list", target.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
// restore published so other tests aren't flaky if shared DB
|
||||
_, _ = doAdminJSON(t, r, http.MethodPatch, "/api/v1/admin/scales/"+target.ID, map[string]string{
|
||||
"status": "published",
|
||||
}, login.Token)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ func doJSONExpect(t *testing.T, r http.Handler, method, path string, body any, d
|
||||
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 {
|
||||
@@ -61,7 +64,8 @@ func createSelfProfile(t *testing.T, r http.Handler, birth, key string) (profile
|
||||
// Flow: star report → gated detail → mock deep unlock
|
||||
func TestFlowStarDeepAccess(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
pid, key := createSelfProfile(t, r, "1983-06-06", "")
|
||||
key := mustRegister(t, r)
|
||||
pid, key := createSelfProfile(t, r, "1983-06-06", key)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/reports/star", map[string]any{
|
||||
"profile_id": pid,
|
||||
@@ -106,7 +110,8 @@ func TestFlowStarDeepAccess(t *testing.T) {
|
||||
// Flow: rhythm report gated + unlock
|
||||
func TestFlowRhythmDeepAccess(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
pid, key := createSelfProfile(t, r, "1992-06-08", "")
|
||||
key := mustRegister(t, r)
|
||||
pid, key := createSelfProfile(t, r, "1992-06-08", key)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodPost, "/api/v1/reports/rhythm", map[string]any{
|
||||
"profile_id": pid,
|
||||
@@ -140,7 +145,8 @@ func TestFlowRhythmDeepAccess(t *testing.T) {
|
||||
// Flow: image card scenes → draw → quota exhaust → depth unlock via mock pay
|
||||
func TestFlowImageCardQuotaAndDepth(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
pid, key := createSelfProfile(t, r, "1990-01-01", "")
|
||||
key := mustRegister(t, r)
|
||||
pid, key := createSelfProfile(t, r, "1990-01-01", key)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodGet, "/api/v1/image-cards/scenes", nil, key)
|
||||
scenes := decodeData[map[string]any](t, env.Data)
|
||||
@@ -199,7 +205,7 @@ func TestFlowImageCardQuotaAndDepth(t *testing.T) {
|
||||
// Flow: solar terms + mood save/read
|
||||
func TestFlowCompanionMood(t *testing.T) {
|
||||
r, _ := setupAPI(t)
|
||||
var key string
|
||||
key := mustRegister(t, r)
|
||||
|
||||
env, key := doJSON(t, r, http.MethodGet, "/api/v1/solar-terms/today", nil, key)
|
||||
term := decodeData[map[string]any](t, env.Data)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func TestFlowSynastryMultiChartsAndInvite(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": "我",
|
||||
@@ -67,9 +67,8 @@ func TestFlowSynastryMultiChartsAndInvite(t *testing.T) {
|
||||
t.Fatal("empty token")
|
||||
}
|
||||
|
||||
env2, key2 := doJSON(t, r, http.MethodGet, "/api/v1/profiles", nil, "")
|
||||
_ = env2
|
||||
env2, key2 = doJSON(t, r, http.MethodGet, "/api/v1/synastry/invites/"+token, nil, key2)
|
||||
env2Key := mustRegister(t, r)
|
||||
env2, key2 := doJSON(t, r, http.MethodGet, "/api/v1/synastry/invites/"+token, nil, env2Key)
|
||||
meta := decodeData[map[string]any](t, env2.Data)
|
||||
if meta["host_name"] == nil {
|
||||
t.Fatal("host_name missing")
|
||||
|
||||
Reference in New Issue
Block a user