test: portsfake + router contract test + handler unit tests (Task 27)
- internal/ports/portsfake: hand-written in-memory fakes for all 9 ports
(Users/Libraries/Books/Progress/Bookmarks/RateLimiter/Scanner/Media/Uploads);
error semantics mirror the real store exactly — pgx.ErrNoRows for misses,
store.ErrLastAdmin guard, and *pgconn.PgError{Code:23505} for unique
violations (ports.IsUniqueViolation only accepts the PgError shape, so the
fakes must produce it to exercise the 409 branch without touching prod code)
- Media/Uploads fakes are hook-programmable: one field per error branch, so a
test can force e.g. CompleteErr=ErrIncomplete without stubbing the rest
- Scanner fake is mutex-guarded + WaitForScan: handler fires ScanLibraryByID
in a goroutine, tests stay deterministic
- router_test.go: TestRouterContract pins all 27 routes — any route table
change now fails the test explicitly
- handler unit tests (~30 cases, no PG/Redis): users CRUD branches (self-delete
400, last-admin 400, dup 409, 204 ok), library reserved names (contract:
code=bad_request message=reserved_name, per original impl), upload sentinel
mapping (413/400/404 per branch)
- NewRouter takes pure port interfaces; main.go distributes *store.Store
across the 5 store ports at the composition root
Full gate green: gofmt, vet, go test -p 1 (real PG+Redis, 0 skip)
This commit is contained in:
@@ -62,7 +62,7 @@ func setupAPI(t *testing.T) (*store.Store, *scanner.Scanner, http.Handler, strin
|
||||
med := media.New(cfg, rdb)
|
||||
up := upload.New(cfg.BooksDir, cfg.UploadMaxMB)
|
||||
sc := scanner.New(st, cfg, rdb, up)
|
||||
r := api.NewRouter(cfg, st, rdb, sc, med, up)
|
||||
r := api.NewRouter(cfg, st, st, st, st, st, rdb, sc, med, up)
|
||||
if u := os.Getenv("REDIS_URL"); u != "" { // 测试卫生: 共享 redis 上重置登录限流桶, 防跨测试累计 429
|
||||
if opt, e := redis.ParseURL(u); e == nil {
|
||||
rc := redis.NewClient(opt)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"booklib/internal/ports"
|
||||
"booklib/internal/upload"
|
||||
)
|
||||
|
||||
// ---------- libraries (Task 27, portsfake) ----------
|
||||
|
||||
func TestUnit_ListLibraries(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.seedLib(t, "comics")
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodGet, "/api/libraries", atok, nil)
|
||||
httpOK(t, w, 200, "list libraries")
|
||||
var libs []map[string]any
|
||||
if err := jsonUnmarshal(w, &libs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(libs) != 1 || libs[0]["name"] != "comics" {
|
||||
t.Fatalf("unexpected libs %v", libs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_CreateLibrary_ReservedName(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
atok := e.token(t, "admin", 1)
|
||||
// 保留名清单以 media/reserved.go 为准(B8);大小写不敏感。
|
||||
// 契约(与重构前一致): code="bad_request", message="reserved_name"
|
||||
for _, name := range []string{"cache", ".uploads", ".trash", "CACHE", "Cache"} {
|
||||
w := e.do(t, http.MethodPost, "/api/libraries", atok, map[string]string{"Name": name})
|
||||
if w.Code != 400 || errCode(t, w) != "bad_request" || !strings.Contains(w.Body.String(), "reserved_name") {
|
||||
t.Fatalf("name=%q want 400/bad_request/reserved_name got %d %s", name, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_CreateLibrary_BadName(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
atok := e.token(t, "admin", 1)
|
||||
for _, name := range []string{"", "..", "///"} {
|
||||
w := e.do(t, http.MethodPost, "/api/libraries", atok, map[string]string{"Name": name})
|
||||
httpOK(t, w, 400, "bad name "+name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_CreateLibrary_Duplicate(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.seedLib(t, "dup")
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries", atok, map[string]string{"Name": "dup"})
|
||||
httpOK(t, w, 409, "duplicate library")
|
||||
if code := errCode(t, w); code != "exists" {
|
||||
t.Fatalf("error code want exists got %q", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_CreateLibrary_OK(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries", atok, map[string]string{"Name": "new lib"})
|
||||
httpOK(t, w, 201, "create library")
|
||||
body := jsonBody(t, w)
|
||||
if body["name"] != "new lib" {
|
||||
t.Fatalf("unexpected body %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_ScanLibrary_NotFound(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries/999/scan", atok, nil)
|
||||
httpOK(t, w, 404, "scan missing library")
|
||||
}
|
||||
|
||||
func TestUnit_ScanLibrary_Accepted(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
libID, _ := e.seedLib(t, "scannable")
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries/"+strconv.FormatInt(libID, 10)+"/scan", atok, nil)
|
||||
httpOK(t, w, 202, "scan accepted")
|
||||
if !e.sc.WaitForScan(1, time.Second) {
|
||||
t.Fatalf("ScanLibraryByID never called; seen=%v", e.sc.Seen())
|
||||
}
|
||||
if seen := e.sc.Seen(); seen[0] != libID {
|
||||
t.Fatalf("scanned lib %d want %d", seen[0], libID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_ScanLibrary_MemberForbidden(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
libID, _ := e.seedLib(t, "nope")
|
||||
mtok := e.token(t, "member", 2)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries/"+strconv.FormatInt(libID, 10)+"/scan", mtok, nil)
|
||||
httpOK(t, w, 403, "member scan")
|
||||
}
|
||||
|
||||
// ---------- uploads: sentinel → status mapping ----------
|
||||
|
||||
func TestUnit_UploadInit_MapsSentinels(t *testing.T) {
|
||||
libIDPath := func(e *testEnv) string {
|
||||
id, _ := e.seedLib(t, "up")
|
||||
return "/api/libraries/" + strconv.FormatInt(id, 10) + "/upload/init"
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
initErr error
|
||||
wantCode int
|
||||
wantErr string
|
||||
}{
|
||||
{"too large", ports.ErrTooLarge, 413, "too_large"},
|
||||
{"bad name", upload.ErrBadName, 400, "bad_request"},
|
||||
{"bad format", upload.ErrBadFormat, 400, "bad_format"},
|
||||
{"bad size", upload.ErrBadSize, 400, "bad_request"},
|
||||
{"bad chunk", upload.ErrBadChunk, 400, "bad_request"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.up.InitErr = tc.initErr
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, libIDPath(e), atok,
|
||||
map[string]any{"name": "b.cbz", "size": 10, "chunkSize": 4})
|
||||
httpOK(t, w, tc.wantCode, "init "+tc.name)
|
||||
if code := errCode(t, w); code != tc.wantErr {
|
||||
t.Fatalf("error code want %q got %q", tc.wantErr, code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_UploadInit_OK(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
id, _ := e.seedLib(t, "up")
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries/"+strconv.FormatInt(id, 10)+"/upload/init", atok,
|
||||
map[string]any{"name": "b.cbz", "size": 10, "chunkSize": 4})
|
||||
httpOK(t, w, 200, "init ok")
|
||||
body := jsonBody(t, w)
|
||||
if body["uploadId"] != e.up.UID {
|
||||
t.Fatalf("uploadId want %q got %v", e.up.UID, body["uploadId"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_UploadInit_BadBody(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
id, _ := e.seedLib(t, "up")
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/libraries/"+strconv.FormatInt(id, 10)+"/upload/init", atok, nil)
|
||||
httpOK(t, w, 400, "init without json body")
|
||||
}
|
||||
|
||||
func TestUnit_UploadStatus_NotFound(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.up.StatusErr = ports.ErrNotFound
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodGet, "/api/uploads/"+e.up.UID, atok, nil)
|
||||
httpOK(t, w, 404, "status missing upload")
|
||||
if code := errCode(t, w); code != "not_found" {
|
||||
t.Fatalf("error code want not_found got %q", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_UploadStatus_OK(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.up.Received = []int64{0, 2}
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodGet, "/api/uploads/"+e.up.UID, atok, nil)
|
||||
httpOK(t, w, 200, "status ok")
|
||||
var body struct {
|
||||
Received []int64 `json:"received"`
|
||||
}
|
||||
if err := jsonUnmarshal(w, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(body.Received) != 2 || body.Received[1] != 2 {
|
||||
t.Fatalf("received want [0 2] got %v", body.Received)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_UploadPart_BadIndex(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPut, "/api/uploads/"+e.up.UID+"/parts/abc", atok, nil)
|
||||
httpOK(t, w, 400, "part bad index")
|
||||
}
|
||||
|
||||
func TestUnit_UploadPart_TooBig(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.up.PutErr = upload.ErrPartTooBig
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPut, "/api/uploads/"+e.up.UID+"/parts/0", atok, nil)
|
||||
httpOK(t, w, 413, "part too big")
|
||||
}
|
||||
|
||||
func TestUnit_UploadComplete_Incomplete(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.seedLib(t, "up") // fake LibraryID=1 与 seed 的第一条对齐
|
||||
e.up.CompleteErr = ports.ErrIncomplete
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/uploads/"+e.up.UID+"/complete", atok, nil)
|
||||
httpOK(t, w, 400, "complete incomplete")
|
||||
}
|
||||
|
||||
func TestUnit_UploadComplete_OK(t *testing.T) {
|
||||
e := newTestEnv(t)
|
||||
e.seedLib(t, "up")
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/uploads/"+e.up.UID+"/complete", atok, nil)
|
||||
httpOK(t, w, 202, "complete ok")
|
||||
body := jsonBody(t, w)
|
||||
if body["path"] != e.up.RelPath {
|
||||
t.Fatalf("path want %q got %v", e.up.RelPath, body["path"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnit_UploadComplete_LibGone(t *testing.T) {
|
||||
e := newTestEnv(t) // LibraryID=1 但库里没有 id=1 → GetLibrary ErrNoRows → 404? dbErr → 500
|
||||
e.up.LibID = 42
|
||||
atok := e.token(t, "admin", 1)
|
||||
w := e.do(t, http.MethodPost, "/api/uploads/"+e.up.UID+"/complete", atok, nil)
|
||||
// dbErr 把 pgx.ErrNoRows 归为 500(非连接类),契约与真库一致
|
||||
if w.Code != 500 {
|
||||
t.Fatalf("lib gone want 500 got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"booklib/cmd/webui/api"
|
||||
"booklib/internal/auth"
|
||||
"booklib/internal/config"
|
||||
"booklib/internal/ports/portsfake"
|
||||
)
|
||||
|
||||
// testEnv 是纯 fake 装配(Task 27):无 PG/Redis。请求走 api.NewRouter 组装的
|
||||
// 真实 engine,因此认证/鉴权中间件、路由匹配与 gin 的 header 落盘行为都被覆盖;
|
||||
// 集成测试(auth_test.go 的 setupAPI)另走真库,两层互补。
|
||||
type testEnv struct {
|
||||
cfg *config.Config
|
||||
router *gin.Engine
|
||||
users *portsfake.Users
|
||||
libs *portsfake.Libraries
|
||||
books *portsfake.Books
|
||||
progress *portsfake.Progress
|
||||
bookmarks *portsfake.Bookmarks
|
||||
rl *portsfake.RateLimiter
|
||||
sc *portsfake.Scanner
|
||||
med *portsfake.Media
|
||||
up *portsfake.Uploads
|
||||
booksDir string
|
||||
}
|
||||
|
||||
func newTestEnv(t *testing.T) *testEnv {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
resolved, err := filepath.EvalSymlinks(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &config.Config{Addr: ":8080", JWTSecret: []byte("s3cret"), UploadMaxMB: 1,
|
||||
ScanInterval: time.Minute, BooksDir: resolved, CacheDir: t.TempDir(),
|
||||
TrustedProxies: []string{"172.16.0.0/12"}}
|
||||
users := portsfake.NewUsers()
|
||||
libs := portsfake.NewLibraries()
|
||||
books := portsfake.NewBooks()
|
||||
progress := portsfake.NewProgress(libs, books)
|
||||
e := &testEnv{
|
||||
cfg: cfg, users: users, libs: libs, books: books,
|
||||
progress: progress, bookmarks: portsfake.NewBookmarks(),
|
||||
rl: portsfake.NewRateLimiter(), sc: portsfake.NewScanner(),
|
||||
med: portsfake.NewMedia(), up: portsfake.NewUploads(),
|
||||
booksDir: resolved,
|
||||
}
|
||||
e.router = api.NewRouter(cfg, users, libs, books, progress, e.bookmarks,
|
||||
e.rl, e.sc, e.med, e.up)
|
||||
return e
|
||||
}
|
||||
|
||||
// token 签发一个带角色的 JWT(uid 固定 1,与 fake 里 seed 的用户对应)。
|
||||
func (e *testEnv) token(t *testing.T, role string, uid int64) string {
|
||||
t.Helper()
|
||||
tok, err := auth.Sign(e.cfg.JWTSecret, uid, role)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
// do 以 Bearer token 走完整 engine;body 非 nil 时按 JSON 发送。
|
||||
func (e *testEnv) do(t *testing.T, method, path, tok string, body any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var reader *bytes.Reader
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reader = bytes.NewReader(b)
|
||||
} else {
|
||||
reader = bytes.NewReader(nil)
|
||||
}
|
||||
req := httptest.NewRequest(method, path, reader)
|
||||
if tok != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
e.router.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// jsonBody 解析响应体为 map。
|
||||
func jsonBody(t *testing.T, w *httptest.ResponseRecorder) map[string]any {
|
||||
t.Helper()
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("unmarshal %q: %v", w.Body.String(), err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// jsonUnmarshal 解析响应体到任意目标(slice/struct)。
|
||||
func jsonUnmarshal(w *httptest.ResponseRecorder, dst any) error {
|
||||
return json.Unmarshal(w.Body.Bytes(), dst)
|
||||
}
|
||||
|
||||
// errCode 取 error.code 字段(错误响应契约的核心)。
|
||||
func errCode(t *testing.T, w *httptest.ResponseRecorder) string {
|
||||
t.Helper()
|
||||
body := jsonBody(t, w)
|
||||
e, _ := body["error"].(map[string]any)
|
||||
if e == nil {
|
||||
t.Fatalf("no error object in %q", w.Body.String())
|
||||
}
|
||||
return fmt.Sprint(e["code"])
|
||||
}
|
||||
|
||||
// seedLib 建一个 root 落在 BooksDir 内的库(fake 行 + 真实目录),返回 id 与 root。
|
||||
func (e *testEnv) seedLib(t *testing.T, name string) (int64, string) {
|
||||
t.Helper()
|
||||
root := filepath.Join(e.booksDir, name)
|
||||
if err := os.MkdirAll(root, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return e.libs.Seed(name, root), root
|
||||
}
|
||||
|
||||
// httpOK 断言状态码,失败时带上响应体便于定位。
|
||||
func httpOK(t *testing.T, w *httptest.ResponseRecorder, want int, what string) {
|
||||
t.Helper()
|
||||
if w.Code != want {
|
||||
t.Fatalf("%s: want %d got %d body=%q", what, want, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user