fix(api): 补齐 bat-api 控制与后端 RPC

This commit is contained in:
2026-07-31 17:01:03 +08:00
parent 20ddd67947
commit 6af7706190
17 changed files with 793 additions and 51 deletions
+137 -1
View File
@@ -548,6 +548,46 @@ func (f *fakeBackend) ResourceManifest(ctx context.Context, offset int, limit in
return f.manifest, nil
}
type controlBackend struct {
*fakeBackend
calls []string
}
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
b.calls = append(b.calls, "daemon.reload")
return &backendrpc.Ack{Command: "reload", Status: "accepted"}, nil
}
func (b *controlBackend) DaemonRestart(ctx context.Context) (*backendrpc.Ack, error) {
b.calls = append(b.calls, "daemon.restart")
return &backendrpc.Ack{Command: "restart", Status: "accepted"}, nil
}
func (b *controlBackend) DaemonRefresh(ctx context.Context, force bool) (*backendrpc.Ack, error) {
b.calls = append(b.calls, "daemon.refresh")
return &backendrpc.Ack{Command: "refresh", Status: "accepted", Force: &force}, nil
}
func (b *controlBackend) ResourceSync(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) {
b.calls = append(b.calls, "resource.sync")
return &backendrpc.TaskAccepted{TaskID: "task-sync-1", Kind: "resource.sync"}, nil
}
func (b *controlBackend) ResourceVerify(ctx context.Context) (*backendrpc.TaskAccepted, error) {
b.calls = append(b.calls, "resource.verify")
return &backendrpc.TaskAccepted{TaskID: "task-verify-1", Kind: "resource.verify"}, nil
}
func (b *controlBackend) ResourceRepair(ctx context.Context) (*backendrpc.TaskAccepted, error) {
b.calls = append(b.calls, "resource.repair")
return &backendrpc.TaskAccepted{TaskID: "task-repair-1", Kind: "resource.repair"}, nil
}
func (b *controlBackend) CatalogRefresh(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) {
b.calls = append(b.calls, "catalog.refresh")
return &backendrpc.TaskAccepted{TaskID: "task-catalog-refresh-1", Kind: "catalog.refresh"}, nil
}
func TestDiscoverCallsStatusBeforeDoctor(t *testing.T) {
root := fixtureRoot(t)
bytes := uint64(20)
@@ -974,7 +1014,103 @@ func TestOpenAPIAndAdminReservedEndpoints(t *testing.T) {
if err := json.Unmarshal(rr.Body.Bytes(), &admin); err != nil {
t.Fatal(err)
}
if admin.Status != "reserved" {
if admin.Status != "available" {
t.Fatalf("admin=%+v", admin)
}
if len(admin.Controls) == 0 || admin.Controls[0] != "/admin/control/reload" {
t.Fatalf("admin controls=%v", admin.Controls)
}
}
func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
cfg := DefaultConfig()
cfg.AuthToken = "control-token"
if err := cfg.Normalize(); err != nil {
t.Fatal(err)
}
backend := &controlBackend{fakeBackend: &fakeBackend{}}
s := NewServer(cfg, backend, nil)
tests := []struct {
name string
action string
body string
rpcMethod string
call string
}{
{name: "reload", action: "reload", rpcMethod: "daemon.reload", call: "daemon.reload"},
{name: "restart", action: "restart", rpcMethod: "daemon.restart", call: "daemon.restart"},
{name: "force sync", action: "sync", body: `{"force":true}`, rpcMethod: "resource.sync", call: "resource.sync"},
{name: "repair", action: "repair", rpcMethod: "resource.repair", call: "resource.repair"},
{name: "catalog refresh", action: "catalog-refresh", rpcMethod: "catalog.refresh", call: "catalog.refresh"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "/admin/control/"+tc.action, strings.NewReader(tc.body))
request.Header.Set("Authorization", "Bearer control-token")
recorder := httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusAccepted {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response AdminControlResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.Action != tc.action || response.RPCMethod != tc.rpcMethod || response.Status != "accepted" {
t.Fatalf("response=%+v", response)
}
if len(backend.calls) == 0 || backend.calls[len(backend.calls)-1] != tc.call {
t.Fatalf("calls=%v", backend.calls)
}
})
}
}
func TestAdminControlRejectsUnauthenticatedDangerousAndUnsupportedActions(t *testing.T) {
cfg := DefaultConfig()
cfg.AuthToken = "control-token"
if err := cfg.Normalize(); err != nil {
t.Fatal(err)
}
backend := &controlBackend{fakeBackend: &fakeBackend{}}
s := NewServer(cfg, backend, nil)
tests := []struct {
name string
action string
token string
body string
wantStatus int
wantCode string
}{
{name: "missing token", action: "repair", wantStatus: http.StatusUnauthorized, wantCode: "unauthorized"},
{name: "dangerous stop", action: "stop", token: "control-token", wantStatus: http.StatusForbidden, wantCode: "control_not_allowed"},
{name: "unknown action", action: "arbitrary-rpc", token: "control-token", wantStatus: http.StatusNotFound, wantCode: "control_not_found"},
{name: "invalid parameters", action: "repair", token: "control-token", body: `{"force":true}`, wantStatus: http.StatusBadRequest, wantCode: "invalid_control_params"},
{name: "restart invalid parameters", action: "restart", token: "control-token", body: `{"force":true}`, wantStatus: http.StatusBadRequest, wantCode: "invalid_control_params"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "/admin/control/"+tc.action, strings.NewReader(tc.body))
if tc.token != "" {
request.Header.Set("Authorization", "Bearer "+tc.token)
}
recorder := httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != tc.wantStatus {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var response ErrorResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if response.Error.Code != tc.wantCode {
t.Fatalf("error=%+v", response.Error)
}
})
}
if len(backend.calls) != 0 {
t.Fatalf("rejected actions reached backend: %v", backend.calls)
}
}