533db535f1
build / build (push) Successful in 1m12s
按已确认需求提供独立安全模型配置、真实探测、正式字典字段评估与原问题译中,持久批次支持只补空白、暂停恢复、有限重试、幂等和逐字段依据。覆盖固定规则、本地语言、组织关联与主表有效看板共用治理侧读模型,保留源生产和冻结历史。 验证:完整 make verify 退出0,702项Go与329项前端通过;默认118项外部/隔离集成明确跳过,另跑完整镜像及组织隔离PG竞态。12并发、输入漂移、过期租约、暂停/恢复及有限重试通过。用户授权的百炼 deepseek-v4.1-flash 完成真实后台与浏览器评估/翻译;552行、16列、11块看板及完整PNG、独立配置页测试/保存、重开恢复和零JS异常通过。仅非业务样本外发,自动处理关闭,原payload不变。 上线前治理库已做导出快照原生备份并真实恢复,60表行数与17个QA表内容哈希一致;不修改DobotAI生产或隧道。本提交时尚未部署,线上验收须独立完成。
79 lines
3.1 KiB
Go
79 lines
3.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"knowledge-governance/internal/dobotaiops"
|
|
"knowledge-governance/internal/qaopsmirror"
|
|
"knowledge-governance/internal/qaopsquality"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type dashboardReaderSpy struct {
|
|
attributedReader
|
|
calls int
|
|
masterCalls int
|
|
query dobotaiops.RecordQuery
|
|
err error
|
|
}
|
|
|
|
func (s *dashboardReaderSpy) ReadDashboard(_ context.Context, q dobotaiops.RecordQuery) (qaopsmirror.Dashboard, error) {
|
|
s.calls++
|
|
s.query = q
|
|
return qaopsmirror.Dashboard{TotalIssues: 7, NewThisWeek: 2}, s.err
|
|
}
|
|
func (s *dashboardReaderSpy) ReadMasterDashboard(ctx context.Context, q dobotaiops.RecordQuery) (qaopsmirror.Dashboard, error) {
|
|
s.masterCalls++
|
|
return s.ReadDashboard(ctx, q)
|
|
}
|
|
func TestQADashboardIsFixedTenantAdminAndReadOnly(t *testing.T) {
|
|
reader := &dashboardReaderSpy{}
|
|
auth := newFakeAuthStore()
|
|
auth.sessions["dash-admin"] = "usr-boss"
|
|
auth.sessions["dash-viewer"] = "usr-eye"
|
|
h, e := NewHandler(Config{Store: &fakeJobStore{}, QAOps: reader, QAOpsTenantKey: "fixed-dashboard-tenant", Now: func() time.Time { return time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC) }, Auth: &AuthConfig{Store: auth}})
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
good := `{"start":"2026-10-05T00:00:00+08:00","end":"2026-10-12T00:00:00+08:00","keyword":"100%_"}`
|
|
for _, c := range []struct {
|
|
token, action, origin, body string
|
|
status int
|
|
}{
|
|
{"", "qa-dashboard", "", good, 401}, {"dash-viewer", "qa-dashboard", "", good, 403}, {"dash-admin", "", "", good, 403}, {"dash-admin", "qa-dashboard", "https://untrusted.invalid", good, 403},
|
|
{"dash-admin", "qa-dashboard", "", `null`, 400}, {"dash-admin", "qa-dashboard", "", `{"start":null,"end":"2026-10-12T00:00:00+08:00"}`, 400},
|
|
{"dash-admin", "qa-dashboard", "", `{"tenant":"other","start":"2026-10-05T00:00:00+08:00","end":"2026-10-12T00:00:00+08:00"}`, 400},
|
|
{"dash-admin", "qa-dashboard", "", good, 200},
|
|
} {
|
|
r := httptest.NewRequest(http.MethodPost, "/api/v1/qa-ops/dashboard", strings.NewReader(c.body))
|
|
if c.token != "" {
|
|
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: c.token})
|
|
}
|
|
r.Header.Set("X-Governance-Action", c.action)
|
|
r.Header.Set("Origin", c.origin)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != c.status {
|
|
t.Fatalf("dashboard status=%d want=%d", w.Code, c.status)
|
|
}
|
|
if w.Code == 200 && w.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatal("private dashboard response may be cached")
|
|
}
|
|
}
|
|
if reader.masterCalls != 1 || reader.calls != 1 || reader.query.Window.TenantKey != "fixed-dashboard-tenant" || reader.query.Keyword != "100%_" {
|
|
t.Fatal("rejected requests reached the reader or browser controlled the tenant")
|
|
}
|
|
reader.err = qaopsquality.ErrConflict
|
|
r := httptest.NewRequest(http.MethodPost, "/api/v1/qa-ops/dashboard", strings.NewReader(good))
|
|
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "dash-admin"})
|
|
r.Header.Set("X-Governance-Action", "qa-dashboard")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("quality input drift status=%d want409", w.Code)
|
|
}
|
|
}
|