package handlers_test import ( "context" "errors" "net/http" "strconv" "testing" "github.com/jackc/pgx/v5" "booklib/internal/auth" ) // ---------- users: admin-only CRUD branches (Task 27, portsfake) ---------- func TestUnit_ListUsers(t *testing.T) { e := newTestEnv(t) hash, _ := auth.HashPassword("password1234") e.users.Seed("admin", hash, "admin") e.users.Seed("member1", hash, "member") atok := e.token(t, "admin", 1) w := e.do(t, http.MethodGet, "/api/users", atok, nil) httpOK(t, w, 200, "list users") var users []map[string]any if err := jsonUnmarshal(w, &users); err != nil { t.Fatal(err) } if len(users) != 2 { t.Fatalf("want 2 users got %d", len(users)) } } func TestUnit_ListUsers_MemberForbidden(t *testing.T) { e := newTestEnv(t) mtok := e.token(t, "member", 2) w := e.do(t, http.MethodGet, "/api/users", mtok, nil) httpOK(t, w, 403, "member list users") } func TestUnit_CreateUser_RoleValidation(t *testing.T) { e := newTestEnv(t) atok := e.token(t, "admin", 1) for _, role := range []string{"superadmin", "", "Member"} { w := e.do(t, http.MethodPost, "/api/users", atok, map[string]string{"Username": "u", "Password": "password1234", "Role": role}) httpOK(t, w, 400, "create user role="+role) } } func TestUnit_CreateUser_ShortPassword(t *testing.T) { e := newTestEnv(t) atok := e.token(t, "admin", 1) w := e.do(t, http.MethodPost, "/api/users", atok, map[string]string{"Username": "u", "Password": "short", "Role": "member"}) httpOK(t, w, 400, "short password") } func TestUnit_CreateUser_DuplicateName(t *testing.T) { e := newTestEnv(t) hash, _ := auth.HashPassword("password1234") e.users.Seed("existing", hash, "member") atok := e.token(t, "admin", 1) w := e.do(t, http.MethodPost, "/api/users", atok, map[string]string{"Username": "existing", "Password": "password1234", "Role": "member"}) httpOK(t, w, 409, "duplicate name") if code := errCode(t, w); code != "exists" { t.Fatalf("error code want 'exists' got %q", code) } } func TestUnit_CreateUser_OK(t *testing.T) { e := newTestEnv(t) atok := e.token(t, "admin", 1) w := e.do(t, http.MethodPost, "/api/users", atok, map[string]string{"Username": "newbie", "Password": "password1234", "Role": "member"}) httpOK(t, w, 201, "create user") body := jsonBody(t, w) if body["username"] != "newbie" || body["role"] != "member" { t.Fatalf("unexpected body %v", body) } } func TestUnit_DeleteUser_BadID(t *testing.T) { e := newTestEnv(t) atok := e.token(t, "admin", 1) w := e.do(t, http.MethodDelete, "/api/users/notanum", atok, nil) httpOK(t, w, 400, "bad id") } func TestUnit_DeleteUser_Self(t *testing.T) { e := newTestEnv(t) uid := e.users.Seed("self", "hash", "admin") atok := e.token(t, "admin", uid) // 自己删自己 w := e.do(t, http.MethodDelete, "/api/users/"+strconv.FormatInt(uid, 10), atok, nil) httpOK(t, w, 400, "delete self") } func TestUnit_DeleteUser_LastAdmin(t *testing.T) { e := newTestEnv(t) uid := e.users.Seed("lastadmin", "hash", "admin") atok := e.token(t, "admin", 999) // 另一个(不存在的)操作者 w := e.do(t, http.MethodDelete, "/api/users/"+strconv.FormatInt(uid, 10), atok, nil) httpOK(t, w, 400, "last admin") } func TestUnit_DeleteUser_NotFound(t *testing.T) { e := newTestEnv(t) e.users.Seed("other", "hash", "admin") atok := e.token(t, "admin", 1) w := e.do(t, http.MethodDelete, "/api/users/99999", atok, nil) httpOK(t, w, 404, "missing user") } func TestUnit_DeleteUser_OK(t *testing.T) { e := newTestEnv(t) e.users.Seed("admin", "hash", "admin") // 保住 last-admin 保护不触发 target := e.users.Seed("todelete", "hash", "member") atok := e.token(t, "admin", 1) w := e.do(t, http.MethodDelete, "/api/users/"+strconv.FormatInt(target, 10), atok, nil) httpOK(t, w, 204, "delete user") if _, err := e.users.GetUserByID(context.Background(), target); !errors.Is(err, pgx.ErrNoRows) { t.Fatal("user should be gone") } } func TestUnit_Me_OK(t *testing.T) { e := newTestEnv(t) uid := e.users.Seed("me", "hash", "member") mtok := e.token(t, "member", uid) w := e.do(t, http.MethodGet, "/api/auth/me", mtok, nil) httpOK(t, w, 200, "me") body := jsonBody(t, w) if body["username"] != "me" { t.Fatalf("username want 'me' got %v", body["username"]) } } func TestUnit_Me_UserGone(t *testing.T) { e := newTestEnv(t) mtok := e.token(t, "member", 99999) w := e.do(t, http.MethodGet, "/api/auth/me", mtok, nil) httpOK(t, w, 401, "me after user gone") } func TestUnit_Me_NoToken(t *testing.T) { e := newTestEnv(t) w := e.do(t, http.MethodGet, "/api/auth/me", "", nil) httpOK(t, w, 401, "me without token") }