User authorized lifting CURRENT FAV whole-branch merge protection on 2026-09-23. Keep FAV_DEGRADED_READ/FAV_PUBLISHER_ENABLED default false. Drop duplicate goose 00034 (local_dev already has 00036). Preserve agentx workspace admin routes and pin-export wiring.
195 lines
6.0 KiB
Go
195 lines
6.0 KiB
Go
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/auth"
|
|
)
|
|
|
|
func loginTS(t *testing.T, baseURL, username, password string) (token string, userID int64) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(map[string]string{"username": username, "password": password})
|
|
resp, err := http.Post(baseURL+"/api/v1/auth/login", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("login: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("login %s: %d", username, resp.StatusCode)
|
|
}
|
|
var session auth.SessionInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&session); err != nil {
|
|
t.Fatalf("decode login: %v", err)
|
|
}
|
|
if session.Token == "" || session.User == nil {
|
|
t.Fatal("empty session")
|
|
}
|
|
return session.Token, session.User.ID
|
|
}
|
|
|
|
func TestUserDirectory_RequiresAuthAndReturnsActiveNames(t *testing.T) {
|
|
ts, _ := setupTestServer(t)
|
|
|
|
unauth, err := http.Get(ts.URL + "/api/v1/users/directory")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unauth.Body.Close()
|
|
if unauth.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401, got %d", unauth.StatusCode)
|
|
}
|
|
|
|
token, _ := loginTS(t, ts.URL, "zhanggong", "123456")
|
|
req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/users/directory", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
var payload struct {
|
|
Users []auth.UserDirectoryEntry `json:"users"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(payload.Users) == 0 {
|
|
t.Fatal("expected non-empty directory")
|
|
}
|
|
foundZhang := false
|
|
for _, u := range payload.Users {
|
|
if u.Name == "" {
|
|
t.Fatalf("directory entry missing name: %+v", u)
|
|
}
|
|
if u.Username == "zhanggong" {
|
|
foundZhang = true
|
|
if u.Name != "张工" {
|
|
t.Fatalf("expected 张工, got %q", u.Name)
|
|
}
|
|
}
|
|
}
|
|
if !foundZhang {
|
|
t.Fatal("expected zhanggong in directory")
|
|
}
|
|
}
|
|
|
|
func TestMeProfile_PatchDepartmentAndClearAdminHint(t *testing.T) {
|
|
ts, _ := setupTestServer(t)
|
|
adminToken, adminID := loginTS(t, ts.URL, "admin", "admin123")
|
|
userToken, userID := loginTS(t, ts.URL, "zhanggong", "123456")
|
|
|
|
// Admin edits target profile → hint set
|
|
patchBody, _ := json.Marshal(map[string]string{
|
|
"name": "张工-管理员改名",
|
|
"department": "骨科耗材研发组",
|
|
"email": "zhang@example.com",
|
|
"employee_id": "RD-001",
|
|
})
|
|
adminReq, _ := http.NewRequest(
|
|
http.MethodPatch,
|
|
ts.URL+"/api/v1/admin/users/"+strconv.FormatInt(userID, 10)+"/profile",
|
|
bytes.NewReader(patchBody),
|
|
)
|
|
adminReq.Header.Set("Authorization", "Bearer "+adminToken)
|
|
adminReq.Header.Set("Content-Type", "application/json")
|
|
adminResp, err := http.DefaultClient.Do(adminReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adminResp.Body.Close()
|
|
if adminResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("admin profile patch: %d %s", adminResp.StatusCode, adminResp.Body)
|
|
}
|
|
|
|
getReq, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/me/profile", nil)
|
|
getReq.Header.Set("Authorization", "Bearer "+userToken)
|
|
getResp, err := http.DefaultClient.Do(getReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer getResp.Body.Close()
|
|
if getResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("me profile get: %d", getResp.StatusCode)
|
|
}
|
|
var profile map[string]any
|
|
if err := json.NewDecoder(getResp.Body).Decode(&profile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if profile["name"] != "张工-管理员改名" {
|
|
t.Fatalf("name after admin edit: %+v", profile["name"])
|
|
}
|
|
if profile["profile_updated_by_name"] == nil || profile["profile_updated_by_name"] == "" {
|
|
t.Fatalf("expected admin hint name, got %+v", profile)
|
|
}
|
|
if by, ok := profile["profile_updated_by"].(float64); !ok || int64(by) != adminID {
|
|
t.Fatalf("expected profile_updated_by=%d, got %+v", adminID, profile["profile_updated_by"])
|
|
}
|
|
|
|
// Self patch clears hint and updates department
|
|
selfBody, _ := json.Marshal(map[string]string{
|
|
"name": "张工",
|
|
"department": "骨科研发一部",
|
|
"email": "zhang.self@example.com",
|
|
})
|
|
selfReq, _ := http.NewRequest(http.MethodPatch, ts.URL+"/api/v1/me/profile", bytes.NewReader(selfBody))
|
|
selfReq.Header.Set("Authorization", "Bearer "+userToken)
|
|
selfReq.Header.Set("Content-Type", "application/json")
|
|
selfResp, err := http.DefaultClient.Do(selfReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer selfResp.Body.Close()
|
|
if selfResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("self profile patch: %d", selfResp.StatusCode)
|
|
}
|
|
|
|
getReq2, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/me/profile", nil)
|
|
getReq2.Header.Set("Authorization", "Bearer "+userToken)
|
|
getResp2, err := http.DefaultClient.Do(getReq2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer getResp2.Body.Close()
|
|
var profile2 map[string]any
|
|
if err := json.NewDecoder(getResp2.Body).Decode(&profile2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if profile2["name"] != "张工" {
|
|
t.Fatalf("name after self save: %+v", profile2["name"])
|
|
}
|
|
if profile2["department"] != "骨科研发一部" {
|
|
t.Fatalf("department after self save: %+v", profile2["department"])
|
|
}
|
|
if profile2["profile_updated_by_name"] != nil {
|
|
t.Fatalf("expected admin hint cleared, got %+v", profile2["profile_updated_by_name"])
|
|
}
|
|
}
|
|
|
|
func TestAdminUpdateProfile_ForbiddenForNonAdmin(t *testing.T) {
|
|
ts, _ := setupTestServer(t)
|
|
userToken, userID := loginTS(t, ts.URL, "zhanggong", "123456")
|
|
body, _ := json.Marshal(map[string]string{"name": "黑客改名"})
|
|
req, _ := http.NewRequest(
|
|
http.MethodPatch,
|
|
ts.URL+"/api/v1/admin/users/"+strconv.FormatInt(userID, 10)+"/profile",
|
|
bytes.NewReader(body),
|
|
)
|
|
req.Header.Set("Authorization", "Bearer "+userToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d", resp.StatusCode)
|
|
}
|
|
}
|