缩略图懒生成 + 评论登录限流:时间线不再拉原图
- 新增 /uploads/thumb/{key}?w=:首访从存储端读一次原图,标准库盒均值缩放后
落盘 data/.thumbnail_cache(按内容哈希命名,天然失效);失败写 .failed 冷却
一小时,非图片/超大一律 302 回原图,前端无感。原图路由改用 ServeContent,
补上视频拖动进度条所需的 Range 支持
- 前端 thumbURL/thumbifyHtml 接入两套 UI 的时间线配图、长文封面与短文正文,
并补 loading=lazy;详情页与灯箱仍用原图。/api/site 改为
{settings, uploads_public_base},让前端识别哪些直链属于自家存储
- 后台登录的滑动窗口限速器抽成 internal/ratelimit 共享包(调用面不变),
新增:读者登录失败按 IP 20 次/10 分钟、评论写入按读者 5 条/分钟
(站主豁免,且只计成功写入)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"oneblog/internal/auth"
|
||||
"oneblog/internal/config"
|
||||
"oneblog/internal/db"
|
||||
"oneblog/internal/model"
|
||||
"oneblog/internal/store"
|
||||
)
|
||||
|
||||
func newTestAPI(t *testing.T) (*API, http.Handler) {
|
||||
t.Helper()
|
||||
d, err := db.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { d.Close() })
|
||||
st, err := store.New(d)
|
||||
if err != nil {
|
||||
t.Fatalf("store: %v", err)
|
||||
}
|
||||
a := &API{
|
||||
Store: st,
|
||||
Cfg: &config.Config{SiteURL: "http://localhost:8080"},
|
||||
ReaderSessions: auth.NewReaderSessions("test-secret", time.Hour),
|
||||
TG: auth.Telegram{Bot: "testbot", Token: "123:abc"},
|
||||
}
|
||||
settings, err := st.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("settings: %v", err)
|
||||
}
|
||||
settings.CommentsEnabled = true
|
||||
if err := st.UpdateSettings(settings); err != nil {
|
||||
t.Fatalf("enable comments: %v", err)
|
||||
}
|
||||
return a, a.Routes()
|
||||
}
|
||||
|
||||
// Telegram 伪造签名反复重试应触发 IP 限速
|
||||
func TestTelegramAuthRateLimited(t *testing.T) {
|
||||
_, h := newTestAPI(t)
|
||||
body := `{"id":1,"first_name":"x","hash":"deadbeef"}`
|
||||
var rec *httptest.ResponseRecorder
|
||||
for i := 0; i < maxAuthFails; i++ {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/telegram", strings.NewReader(body))
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("attempt %d: got %d, want 403", i+1, rec.Code)
|
||||
}
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/telegram", strings.NewReader(body))
|
||||
rec = httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("after %d failures: got %d, want 429", maxAuthFails, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// 评论写入按读者限速:第 maxComments+1 条被拒
|
||||
func TestCommentRateLimited(t *testing.T) {
|
||||
a, h := newTestAPI(t)
|
||||
p, err := a.Store.Create(model.PostInput{Kind: model.KindLong, Title: "t", Slug: "t",
|
||||
ContentMd: "x", Status: model.StatusPublished})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rd, err := a.Store.UpsertReader(model.Reader{Provider: "github", Handle: "u1", Name: "u1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tok, _ := a.ReaderSessions.Issue(rd.ID)
|
||||
post := func(i int) *httptest.ResponseRecorder {
|
||||
body, _ := json.Marshal(map[string]any{"post_id": p.ID, "body_md": "好"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/comments", strings.NewReader(string(body)))
|
||||
req.AddCookie(&http.Cookie{Name: auth.ReaderCookie, Value: tok})
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
for i := 0; i < maxComments; i++ {
|
||||
if rec := post(i); rec.Code != http.StatusCreated {
|
||||
t.Fatalf("comment %d: got %d %s", i+1, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
if rec := post(maxComments); rec.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("over limit: got %d, want 429", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user