package gateway import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" ) // listBackthinkItems 调用管理端点并把线上结构解码回来。 func listBackthinkItems(t *testing.T, ts *httptest.Server, tok string) (int, []byte, []AdminBackthinkItem) { t.Helper() resp, raw := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/backthink", nil, tok) var list struct { Data []AdminBackthinkItem `json:"data"` } if resp.StatusCode == http.StatusOK { if err := json.Unmarshal(raw, &list); err != nil { t.Fatalf("decode: %v (%s)", err, raw) } } return resp.StatusCode, raw, list.Data } // TestAdminBackthink_ExposesPushRouteDetailBypass 是 P0-20 的真断言: // GET /api/v1/admin/backthink 必须把后端已存的 PushResult 里 // route/detail/bypassed 真的送到客户端 —— 面板才能解释「为什么打扰 / // 为什么没打扰」。断言同时落在原始 JSON 与解码后的值上。 func TestAdminBackthink_ExposesPushRouteDetailBypass(t *testing.T) { t.Parallel() feed := &stubBackthinkFeed{records: []backthink.Record{ { ID: 11, Trigger: "event", At: time.UnixMilli(1700), Conclusion: "服务器告警", ShouldPush: true, PushResult: backthink.PushResult{Pushed: true, Route: backthink.RouteActiveClient, Bypassed: true, Detail: "2 active clients"}, }, { ID: 10, Trigger: "heartbeat", At: time.UnixMilli(1600), Conclusion: "只是想了一下", ShouldPush: false, PushResult: backthink.PushResult{Pushed: false, Route: backthink.RouteRateLimited, Detail: "proactive push rate limited"}, }, }} _, ts, jwt := buildEnv(t, nil, Deps{Backthink: feed}) status, raw, items := listBackthinkItems(t, ts, signAny(t, jwt, "acct", auth.RoleOwner)) if status != http.StatusOK { t.Fatalf("status = %d, body = %s", status, raw) } for _, want := range []string{ `"route":"active_client"`, `"route":"rate_limited"`, `"detail":"proactive push rate limited"`, `"bypassed":true`, } { if !strings.Contains(string(raw), want) { t.Fatalf("response is missing %s: %s", want, raw) } } if len(items) != 2 { t.Fatalf("items = %d, want 2", len(items)) } got := items[0] if got.ID != "11" || got.Route != string(backthink.RouteActiveClient) || got.Detail != "2 active clients" || !got.Bypassed || !got.Pushed { t.Fatalf("item[0] = %+v", got) } silent := items[1] if silent.Route != string(backthink.RouteRateLimited) || silent.Detail == "" || silent.Bypassed || silent.Pushed { t.Fatalf("item[1] = %+v", silent) } } // TestAdminBackthink_OmitsEmptyRouteAndDetail 钉住「没有值就不编」:未尝试 // 推送(零值 PushResult)的记录不带 route/detail 键,而 bypassed 作为 // 布尔始终显式出现 —— 面板因此能区分「这次没有路由」与「字段没接线」。 func TestAdminBackthink_OmitsEmptyRouteAndDetail(t *testing.T) { t.Parallel() feed := &stubBackthinkFeed{records: []backthink.Record{ {ID: 9, Trigger: "idle", At: time.UnixMilli(900), Conclusion: "什么都没有", ShouldPush: false}, }} _, ts, jwt := buildEnv(t, nil, Deps{Backthink: feed}) status, raw, items := listBackthinkItems(t, ts, signAny(t, jwt, "acct", auth.RoleOwner)) if status != http.StatusOK { t.Fatalf("status = %d, body = %s", status, raw) } if len(items) != 1 { t.Fatalf("items = %d, want 1", len(items)) } it := items[0] if it.Route != "" || it.Detail != "" || it.Bypassed || it.Pushed || it.ShouldNotify { t.Fatalf("item = %+v, want zero push fields", it) } if strings.Contains(string(raw), `"route"`) || strings.Contains(string(raw), `"detail"`) { t.Fatalf("empty route/detail must be omitted: %s", raw) } if !strings.Contains(string(raw), `"bypassed":false`) { t.Fatalf("bypassed must always be present: %s", raw) } }