package integration_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" ) func doAdminJSON(t *testing.T, r http.Handler, method, path string, body any, token string) (envelope, int) { t.Helper() auth := "" if token != "" { auth = "Bearer " + token } return doAdminAuth(t, r, method, path, body, auth) } func doAdminAuth(t *testing.T, r http.Handler, method, path string, body any, authorization string) (envelope, int) { t.Helper() var buf bytes.Buffer if body != nil { if err := json.NewEncoder(&buf).Encode(body); err != nil { t.Fatalf("encode: %v", err) } } req := httptest.NewRequest(method, path, &buf) req.Header.Set("Content-Type", "application/json") if authorization != "" { req.Header.Set("Authorization", authorization) } w := httptest.NewRecorder() r.ServeHTTP(w, req) var env envelope _ = json.Unmarshal(w.Body.Bytes(), &env) return env, w.Code } func TestAdminOpsPhaseA(t *testing.T) { r, _ := setupAPI(t) env, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users", nil, "") if code != http.StatusUnauthorized || env.Code == 0 { t.Fatalf("expected 401 without token, got http=%d code=%d", code, env.Code) } env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/auth/login", map[string]string{ "username": "admin", "password": "change-me", }, "") if code != 200 || env.Code != 0 { t.Fatalf("login failed http=%d code=%d msg=%s body=%s", code, env.Code, env.Message, string(env.Data)) } var login struct { Token string `json:"token"` } if err := json.Unmarshal(env.Data, &login); err != nil || login.Token == "" { t.Fatalf("login token missing: %v %s", err, env.Data) } _, _ = doJSON(t, r, http.MethodGet, "/api/v1/profiles", nil, "") env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users", nil, login.Token) if code != 200 || env.Code != 0 { t.Fatalf("list users failed http=%d code=%d msg=%s", code, env.Code, env.Message) } var list struct { Items []struct { ID string `json:"id"` } `json:"items"` } if err := json.Unmarshal(env.Data, &list); err != nil || len(list.Items) == 0 { t.Fatalf("expected users, got %v %s", err, env.Data) } userID := list.Items[0].ID env, code = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/users/"+userID+"/membership/grant", map[string]string{ "plan": "month", }, login.Token) if code != 200 || env.Code != 0 { t.Fatalf("grant failed http=%d code=%d msg=%s", code, env.Code, env.Message) } // Atomicity: membership active AND audit row for same grant. env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/users/"+userID, nil, login.Token) if code != 200 || env.Code != 0 { t.Fatalf("get user failed http=%d code=%d msg=%s", code, env.Code, env.Message) } var detail struct { Membership struct { Active bool `json:"active"` Status string `json:"status"` } `json:"membership"` } if err := json.Unmarshal(env.Data, &detail); err != nil || !detail.Membership.Active { t.Fatalf("expected active membership after grant: %v %s", err, env.Data) } env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/audit-logs", nil, login.Token) if code != 200 || env.Code != 0 { t.Fatalf("audit failed http=%d code=%d msg=%s", code, env.Code, env.Message) } var audit struct { Items []struct { Action string `json:"action"` TargetID string `json:"target_id"` } `json:"items"` } if err := json.Unmarshal(env.Data, &audit); err != nil || len(audit.Items) == 0 { t.Fatalf("expected audit rows: %v %s", err, env.Data) } if audit.Items[0].Action != "membership.grant" || audit.Items[0].TargetID != userID { t.Fatalf("unexpected audit %#v", audit.Items[0]) } // Logout with lowercase bearer must invalidate session. env, code = doAdminAuth(t, r, http.MethodPost, "/api/v1/admin/auth/logout", nil, "bearer "+login.Token) if code != 200 || env.Code != 0 { t.Fatalf("logout failed http=%d code=%d msg=%s", code, env.Code, env.Message) } env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/me", nil, login.Token) if code != http.StatusUnauthorized || env.Code == 0 { t.Fatalf("expected 401 after logout, got http=%d code=%d", code, env.Code) } }