feat(backend): book list/detail/delete with embedded progress + cache cleanup
This commit is contained in:
@@ -19,7 +19,7 @@ import (
|
||||
"booklib/internal/store"
|
||||
)
|
||||
|
||||
func setupAPI(t *testing.T) (*store.Store, http.Handler) {
|
||||
func setupAPI(t *testing.T) (*store.Store, *scanner.Scanner, http.Handler, string) {
|
||||
t.Helper()
|
||||
url := os.Getenv("DATABASE_URL")
|
||||
if url == "" {
|
||||
@@ -32,20 +32,24 @@ func setupAPI(t *testing.T) (*store.Store, http.Handler) {
|
||||
}
|
||||
st := store.New(p)
|
||||
p.Exec(ctx, "DELETE FROM reading_progress; DELETE FROM books; DELETE FROM libraries; DELETE FROM users")
|
||||
h, _ := auth.HashPassword("pw12345")
|
||||
_, err := st.CreateUser(ctx, "alice", h, "admin")
|
||||
if err != nil {
|
||||
h, _ := auth.HashPassword(testPW)
|
||||
if _, err := st.CreateUser(ctx, "alice", h, "admin"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = st.CreateUser(ctx, "bob", h, "member")
|
||||
if _, err := st.CreateUser(ctx, "bob", h, "member"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
booksParent := t.TempDir()
|
||||
booksDir, err := filepath.EvalSymlinks(booksParent) // macOS 上 /var→/private,root 校验要用真实路径
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := testCfg()
|
||||
cfg.BooksDir = filepath.Clean(os.TempDir())
|
||||
cfg.BooksDir = booksDir
|
||||
cfg.CacheDir = t.TempDir()
|
||||
rdb := redispkg.New(os.Getenv("REDIS_URL"))
|
||||
r := NewRouter(cfg, st, rdb, scanner.New(st, cfg, rdb))
|
||||
sc := scanner.New(st, cfg, rdb)
|
||||
r := NewRouter(cfg, st, rdb, sc)
|
||||
if u := os.Getenv("REDIS_URL"); u != "" { // 测试卫生: 共享 redis 上重置登录限流桶, 防跨测试累计 429
|
||||
if opt, e := redis.ParseURL(u); e == nil {
|
||||
rc := redis.NewClient(opt)
|
||||
@@ -53,9 +57,12 @@ func setupAPI(t *testing.T) (*store.Store, http.Handler) {
|
||||
rc.Close()
|
||||
}
|
||||
}
|
||||
return st, r
|
||||
return st, sc, r, booksDir
|
||||
}
|
||||
|
||||
// testPW 是唯一的 fixture 口令常量: 直接种子 (HashPassword) 与所有登录/建户必须同值, 且 >=8 位
|
||||
const testPW = "pw123456"
|
||||
|
||||
func do(h http.Handler, method, path, token string, body any) *httptest.ResponseRecorder {
|
||||
var r *bytes.Reader
|
||||
if body != nil {
|
||||
@@ -74,8 +81,8 @@ func do(h http.Handler, method, path, token string, body any) *httptest.Response
|
||||
}
|
||||
|
||||
func TestLoginMe(t *testing.T) {
|
||||
_, h := setupAPI(t)
|
||||
w := do(h, "POST", "/api/auth/login", "", map[string]string{"username": "alice", "password": "pw12345"})
|
||||
_, _, h, _ := setupAPI(t)
|
||||
w := do(h, "POST", "/api/auth/login", "", map[string]string{"username": "alice", "password": testPW})
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("login %d %s", w.Code, w.Body)
|
||||
}
|
||||
@@ -105,13 +112,12 @@ func TestLoginMe(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemberCannotWriteUsers(t *testing.T) {
|
||||
_, h := setupAPI(t)
|
||||
tok, _ := auth.Sign([]byte("s3cret"), 2, "member") // bob — 注意: 必须走真实登录拿 token
|
||||
w := do(h, "POST", "/api/auth/login", "", map[string]string{"username": "bob", "password": "pw12345"})
|
||||
_, _, h, _ := setupAPI(t)
|
||||
w := do(h, "POST", "/api/auth/login", "", map[string]string{"username": "bob", "password": testPW})
|
||||
var v struct{ Token string }
|
||||
json.Unmarshal(w.Body.Bytes(), &v)
|
||||
tok = v.Token
|
||||
w = do(h, "POST", "/api/users", tok, map[string]string{"username": "eve", "password": "pw12345", "role": "member"})
|
||||
tok := v.Token
|
||||
w = do(h, "POST", "/api/users", tok, map[string]string{"username": "eve", "password": testPW, "role": "member"})
|
||||
if w.Code != 403 {
|
||||
t.Fatalf("member write users: want 403 got %d", w.Code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user