package gateway import ( "encoding/json" "net/http" "net/http/httptest" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" ) // These tests cover the shared client enum endpoint (Android collaboration B4, // design/07 ยง6): the wire shape of GET /api/v1/meta/enums, the values it // publishes, and the fact that it is readable by any authenticated role. They // assert the JSON contract with literals rather than by comparing against // internal/contract/enums, so the client-facing shape is pinned independently of // the Go table (the table's own drift tests live in that package). // enumEntry is the wire shape of one published enum item. type enumEntry struct { ID string `json:"id"` Label string `json:"label"` } // metaEnumsWire decodes GET /api/v1/meta/enums and asserts the envelope and the // per-item shape, returning the ids of each table for value assertions. func metaEnumsWire(t *testing.T, ts *httptest.Server, tok string) (platforms, moods, modes []string, unknown string) { t.Helper() resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/meta/enums", nil, tok) if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) } env := decodeBody(t, body) if len(env) != 1 { t.Fatalf("envelope keys = %v, want only data", env) } var data struct { Platforms []enumEntry `json:"platforms"` Moods []enumEntry `json:"moods"` Modes []enumEntry `json:"modes"` Unknown string `json:"unknown"` } if err := json.Unmarshal(env["data"], &data); err != nil { t.Fatalf("decode data: %v (body=%s)", err, body) } for _, table := range []struct { name string entries []enumEntry }{{"platforms", data.Platforms}, {"moods", data.Moods}, {"modes", data.Modes}} { if len(table.entries) == 0 { t.Fatalf("%s is empty (body=%s)", table.name, body) } for _, e := range table.entries { if e.ID == "" || e.Label == "" { t.Errorf("%s entry %+v must carry both id and label", table.name, e) } } } return entryIDs(data.Platforms), entryIDs(data.Moods), entryIDs(data.Modes), data.Unknown } func entryIDs(items []enumEntry) []string { out := make([]string, 0, len(items)) for _, it := range items { out = append(out, it.ID) } return out } // TestMetaEnumsShape pins the published contract: the three tables name the real // platform/mood/mode values and the response carries the unknown-value fallback // policy. func TestMetaEnumsShape(t *testing.T) { t.Parallel() _, ts, jwt := buildEnv(t, cfgDefault(), Deps{}) tok := signAny(t, jwt, "acct", auth.RoleOwner) platforms, moods, modes, unknown := metaEnumsWire(t, ts, tok) wantPlatforms := []string{"web", "kook", "qq-obv11", "weixin-ilink", "telegram", "webhook"} if !sameStringSet(platforms, wantPlatforms) { t.Errorf("platforms = %v, want %v", platforms, wantPlatforms) } wantMoods := []string{"calm", "happy", "excited", "sad", "angry"} if !sameStringSet(moods, wantMoods) { t.Errorf("moods = %v, want %v", moods, wantMoods) } wantModes := []string{"chat", "code", "home", "work", "docs", "voice", "vision", "agents", "settings"} if !sameStringSet(modes, wantModes) { t.Errorf("modes = %v, want %v", modes, wantModes) } if unknown != "display-raw" { t.Errorf("unknown = %q, want display-raw", unknown) } } // TestMetaEnumsAnyAuthenticatedRole asserts the endpoint is a read-only, // non-sensitive table: every role may read it, and anonymous callers get 401 // rather than the data. func TestMetaEnumsAnyAuthenticatedRole(t *testing.T) { t.Parallel() _, ts, jwt := buildEnv(t, cfgDefault(), Deps{}) url := ts.URL + "/api/v1/meta/enums" for _, role := range []auth.Role{auth.RoleOwner, auth.RoleAdmin, auth.RoleViewer} { tok := signAny(t, jwt, "acct", role) resp, body := doReq(t, http.MethodGet, url, nil, tok) if resp.StatusCode != http.StatusOK { t.Fatalf("role %s status = %d, want 200 (body=%s)", role, resp.StatusCode, body) } } resp, body := doReq(t, http.MethodGet, url, nil, "") if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("anonymous status = %d, want 401 (body=%s)", resp.StatusCode, body) } wantErrorShape(t, body) } // sameStringSet reports whether got and want hold the same values, ignoring order. func sameStringSet(got, want []string) bool { if len(got) != len(want) { return false } seen := make(map[string]int, len(got)) for _, v := range got { seen[v]++ } for _, v := range want { seen[v]-- } for _, n := range seen { if n != 0 { return false } } return true }