- 生效规则统一为「后台填了用后台的,没填回落环境变量」,老部署不改 env 照常跑 - config.Resolver:短缓存解析有效配置,存储后端按配置签名热重建;后台保存主动失效 - 秘密项(client secret / bot token / R2 密钥 / 管理员密码)接口永不回显明文, 只报「是否已配置、来自哪里」;留空保存 = 保持现值 - 管理员密码 bcrypt 入库,DB 哈希优先、显式设置的 env 密码作解锁后路; 后台改过密码后 admin/admin 开发模式立即失效 - 设置页新增「登录与存储」标签,基础信息加站点地址;秘密项带来源提示 - 监听地址 / 数据库 / 目录 / ONE_SECRET / Passkey 仍留环境变量(启动期依赖)
97 lines
2.8 KiB
Go
97 lines
2.8 KiB
Go
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",
|
|
TelegramBot: "testbot", TelegramToken: "123:abc"},
|
|
ReaderSessions: auth.NewReaderSessions("test-secret", time.Hour),
|
|
}
|
|
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)
|
|
}
|
|
}
|