167 lines
6.4 KiB
Go
167 lines
6.4 KiB
Go
package adminweb
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/settings"
|
|
)
|
|
|
|
type fakeService struct {
|
|
snapshot Snapshot
|
|
verified bool
|
|
saved bool
|
|
disabled string
|
|
disableErr error
|
|
}
|
|
|
|
func (f *fakeService) Snapshot(context.Context) (Snapshot, error) { return f.snapshot, nil }
|
|
func (f *fakeService) SaveCredentials(context.Context, string, string) error {
|
|
f.saved = true
|
|
return nil
|
|
}
|
|
func (f *fakeService) VerifyCredentials(context.Context, string, string) Verification {
|
|
f.verified = true
|
|
return Verification{OK: true, Code: "ok", CheckedAt: time.Unix(1, 0)}
|
|
}
|
|
func (f *fakeService) DisableAdministrator(_ context.Context, staffID string) error {
|
|
f.disabled = staffID
|
|
return f.disableErr
|
|
}
|
|
func csrfTokenFromPage(t *testing.T, h http.Handler) string {
|
|
t.Helper()
|
|
get := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
|
|
get.Header.Set("Authorization", "Bearer admin-token")
|
|
page := httptest.NewRecorder()
|
|
h.ServeHTTP(page, get)
|
|
marker := `name="csrf_token" value="`
|
|
start := strings.Index(page.Body.String(), marker)
|
|
if start < 0 {
|
|
t.Fatalf("csrf token missing: %s", page.Body.String())
|
|
}
|
|
start += len(marker)
|
|
end := strings.Index(page.Body.String()[start:], `"`)
|
|
if end < 0 {
|
|
t.Fatal("csrf token terminator missing")
|
|
}
|
|
return page.Body.String()[start : start+end]
|
|
}
|
|
func TestDashboardNeverRendersSecrets(t *testing.T) {
|
|
svc := &fakeService{snapshot: Snapshot{SystemState: "active", ClientIDMasked: "ding***", SecretConfigured: true}}
|
|
h := New(svc, "admin-token")
|
|
req := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
if w.Code != 200 {
|
|
t.Fatalf("status=%d", w.Code)
|
|
}
|
|
body := w.Body.String()
|
|
if strings.Contains(body, "admin-token") || strings.Contains(strings.ToLower(body), "client secret value") {
|
|
t.Fatal("secret leaked")
|
|
}
|
|
if !strings.Contains(body, "ding***") {
|
|
t.Fatal("masked client id missing")
|
|
}
|
|
}
|
|
func TestDashboardRendersOperationalComponentFields(t *testing.T) {
|
|
checked := time.Date(2026, 8, 13, 18, 0, 0, 0, time.UTC)
|
|
svc := &fakeService{snapshot: Snapshot{Components: []Component{{
|
|
Name: "WeKnora", State: "ok", Code: "health_ok", Detail: "HTTP健康探针正常",
|
|
LastSuccess: checked.Add(-time.Minute), CheckedAt: checked,
|
|
}}}}
|
|
h := New(svc, "admin-token")
|
|
req := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
body := w.Body.String()
|
|
for _, want := range []string{"WeKnora", "health_ok", "HTTP健康探针正常", "2026-08-13 17:59:00", "2026-08-13 18:00:00"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("dashboard missing %q: %s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSaveCredentialsRequiresVerification(t *testing.T) {
|
|
svc := &fakeService{}
|
|
h := New(svc, "admin-token")
|
|
csrf := csrfTokenFromPage(t, h)
|
|
req := httptest.NewRequest(http.MethodPost, "/admin/setup/credentials", strings.NewReader("client_id=id&client_secret=secret&csrf_token="+csrf))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
if w.Code != http.StatusSeeOther || !svc.verified || !svc.saved {
|
|
t.Fatalf("status=%d verified=%v saved=%v", w.Code, svc.verified, svc.saved)
|
|
}
|
|
}
|
|
|
|
func TestAllAdminPOSTRoutesRejectMissingCSRF(t *testing.T) {
|
|
svc := &fakeService{}
|
|
h := New(svc, "admin-token")
|
|
for _, route := range []string{"/admin/setup/credentials", "/admin/setup/candidate/activate", "/admin/setup/bootstrap", "/admin/setup/bootstrap/confirm", "/admin/settings/administrators/disable"} {
|
|
req := httptest.NewRequest(http.MethodPost, route, strings.NewReader("client_id=id&client_secret=secret&code=123456&staff_id=staff-old"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("route=%s status=%d body=%q", route, w.Code, w.Body.String())
|
|
}
|
|
}
|
|
if svc.saved || svc.disabled != "" {
|
|
t.Fatalf("business service called saved=%v disabled=%q", svc.saved, svc.disabled)
|
|
}
|
|
}
|
|
|
|
func TestDashboardRendersAdministratorReplacementWorkflow(t *testing.T) {
|
|
svc := &fakeService{snapshot: Snapshot{Administrators: []AdministratorView{{StaffID: "staff-old", StaffIDMasked: "sta***old", DisplayName: "旧管理员", Department: "技术部", Role: "super_admin", Status: "active", CanDisable: true}}}}
|
|
h := New(svc, "admin-token")
|
|
req := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
for _, want := range []string{"新增/更换管理员", "先加后停", "旧管理员", "技术部", "sta***old", "生成6位绑定码", "停用此管理员"} {
|
|
if !strings.Contains(w.Body.String(), want) {
|
|
t.Fatalf("dashboard missing %q: %s", want, w.Body.String())
|
|
}
|
|
}
|
|
if strings.Contains(w.Body.String(), ">staff-old<") {
|
|
t.Fatalf("raw staff ID must not be rendered as visible text: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDisableAdministratorRequiresCSRFAndMapsLastAdminConflict(t *testing.T) {
|
|
svc := &fakeService{}
|
|
h := New(svc, "admin-token")
|
|
post := func(body string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodPost, "/admin/settings/administrators/disable", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
if w := post("staff_id=staff-old"); w.Code != http.StatusForbidden || svc.disabled != "" {
|
|
t.Fatalf("missing csrf status=%d disabled=%q", w.Code, svc.disabled)
|
|
}
|
|
|
|
csrf := csrfTokenFromPage(t, h)
|
|
|
|
svc.disableErr = settings.ErrLastAdministrator
|
|
w := post("staff_id=staff-old&csrf_token=" + csrf)
|
|
if w.Code != http.StatusConflict || !strings.Contains(w.Body.String(), "至少保留一位有效管理员") {
|
|
t.Fatalf("last-admin status=%d body=%q", w.Code, w.Body.String())
|
|
}
|
|
svc.disableErr = nil
|
|
w = post("staff_id=staff-old&csrf_token=" + csrf)
|
|
if w.Code != http.StatusSeeOther || svc.disabled != "staff-old" {
|
|
t.Fatalf("success status=%d disabled=%q body=%q", w.Code, svc.disabled, w.Body.String())
|
|
}
|
|
}
|