feat(server): public /maintenance/site-status route + tests; P9 evidence

- isPublicRoute exempts site-status (anonymous banner polling)
- httptest coverage: anonymous 200 + site_notice payload; route table guards
- test report + playwright evidence (docker mcr playwright v1.63 noble vs real dev stack, 1 passed)
This commit is contained in:
weichen
2026-09-25 16:33:58 +00:00
parent 914d0aa39d
commit 923da11ab4
3 changed files with 155 additions and 1 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ func isPublicRoute(method, path string) bool {
if path == "/" || path == "/health" || path == "/api/v1/health" ||
path == "/ready" || path == "/api/v1/ready" ||
path == "/api/v1/vault/status" ||
path == "/api/v1/maintenance/status" {
path == "/api/v1/maintenance/status" ||
path == "/api/v1/maintenance/site-status" {
return true
}
if method == http.MethodPost && path == "/api/v1/auth/login" {
@@ -0,0 +1,73 @@
package server
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"chat-agent-system/internal/maintenance"
)
// P9:site-status 为公开端点(登录页横幅轮询),匿名可达且不落鉴权错误。
func TestMaintenanceSiteStatusPublic(t *testing.T) {
svc := maintenance.NewService(&fakeSiteStore{})
svc.SetSiteSettings(func(ctx context.Context) (string, bool) {
return "公告内容", true
})
s := &Server{}
s.setOverrideMaintenance(svc)
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/maintenance/site-status", s.handleMaintenanceSiteStatus)
ts := httptest.NewServer(mux)
defer ts.Close()
resp, err := http.Get(ts.URL + "/api/v1/maintenance/site-status")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var view maintenance.StatusView
if err := json.NewDecoder(resp.Body).Decode(&view); err != nil {
t.Fatal(err)
}
if view.Notice != "公告内容" || !view.ReadOnly || view.Status != "site_notice" {
t.Fatalf("site view = %+v", view)
}
}
// isPublicRoute 必须豁免 site-status(匿名横幅)。
func TestIsPublicRouteMaintenanceSiteStatus(t *testing.T) {
if !isPublicRoute(http.MethodGet, "/api/v1/maintenance/site-status") {
t.Fatal("site-status must be public")
}
if !isPublicRoute(http.MethodGet, "/api/v1/maintenance/status") {
t.Fatal("status must stay public")
}
if isPublicRoute(http.MethodGet, "/api/v1/admin/maintenance") {
t.Fatal("admin maintenance must not be public")
}
}
type fakeSiteStore struct{}
func (f *fakeSiteStore) GetOpen(ctx context.Context) (*maintenance.Window, error) {
return nil, maintenance.ErrNotFound
}
func (f *fakeSiteStore) GetByID(ctx context.Context, id int64) (*maintenance.Window, error) {
return nil, maintenance.ErrNotFound
}
func (f *fakeSiteStore) Insert(ctx context.Context, w maintenance.Window) (maintenance.Window, error) {
return w, nil
}
func (f *fakeSiteStore) UpdateStatus(ctx context.Context, id int64, from, to string) (maintenance.Window, error) {
return maintenance.Window{}, nil
}
func (f *fakeSiteStore) ListRecent(ctx context.Context, limit int) ([]maintenance.Window, error) {
return nil, nil
}