Files
Duy /zuey/ 990fabf94b fix(config): persist shell deny group disables
Squash merge PR #112 after resolving the project changelog conflict with current dev. Local checks passed: config gateway tests, provider/http/tools deny-pattern tests, go build ./..., and go build -tags sqliteonly ./.... PR CI run 26704832350 passed release-versioning, go, and web.
2026-05-31 13:08:24 +07:00

148 lines
4.0 KiB
Go

package methods
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/gateway"
"github.com/nextlevelbuilder/goclaw/internal/permissions"
"github.com/nextlevelbuilder/goclaw/internal/store"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
func TestConfigPatchPersistsInboundDebounceMs(t *testing.T) {
t.Parallel()
cfg := config.Default()
cfgPath := filepath.Join(t.TempDir(), "config.json")
methods := NewConfigMethods(cfg, cfgPath, nil, nil)
client, responses := gateway.NewCapturingTestClient(permissions.RoleOwner, store.MasterTenantID, "owner", 1)
params, err := json.Marshal(map[string]string{
"raw": `{"gateway":{"inbound_debounce_ms":500}}`,
})
if err != nil {
t.Fatal(err)
}
methods.handlePatch(
store.WithTenantID(context.Background(), store.MasterTenantID),
client,
&protocol.RequestFrame{
Type: protocol.FrameTypeRequest,
ID: "patch-inbound-debounce",
Method: protocol.MethodConfigPatch,
Params: params,
},
)
res := readConfigPatchResponse(t, responses)
if !res.OK {
t.Fatalf("config.patch failed: %#v", res.Error)
}
if cfg.Gateway.InboundDebounceMs != 500 {
t.Fatalf("in-memory inbound_debounce_ms = %d, want 500", cfg.Gateway.InboundDebounceMs)
}
data, err := os.ReadFile(cfgPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(data, []byte(`"inbound_debounce_ms": 500`)) {
t.Fatalf("saved config missing inbound_debounce_ms=500:\n%s", data)
}
}
func TestConfigPatchPersistsShellDenyGroupsFalse(t *testing.T) {
t.Parallel()
cfg := config.Default()
cfgPath := filepath.Join(t.TempDir(), "config.json")
methods := NewConfigMethods(cfg, cfgPath, nil, nil)
client, responses := gateway.NewCapturingTestClient(permissions.RoleOwner, store.MasterTenantID, "owner", 1)
params, err := json.Marshal(map[string]string{
"raw": `{"tools":{"shellDenyGroups":{"package_install":false}}}`,
})
if err != nil {
t.Fatal(err)
}
ctx := store.WithTenantID(context.Background(), store.MasterTenantID)
methods.handlePatch(
ctx,
client,
&protocol.RequestFrame{
Type: protocol.FrameTypeRequest,
ID: "patch-shell-deny-groups",
Method: protocol.MethodConfigPatch,
Params: params,
},
)
res := readConfigPatchResponse(t, responses)
if !res.OK {
t.Fatalf("config.patch failed: %#v", res.Error)
}
shellDenyGroups := cfg.ShellDenyGroupsSnapshot()
if v, ok := shellDenyGroups["package_install"]; !ok || v {
t.Fatalf("in-memory shellDenyGroups package_install = %v (ok=%v), want false", v, ok)
}
data, err := os.ReadFile(cfgPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(data, []byte(`"package_install": false`)) {
t.Fatalf("saved config missing package_install=false:\n%s", data)
}
methods.handleGet(
ctx,
client,
&protocol.RequestFrame{
Type: protocol.FrameTypeRequest,
ID: "get-shell-deny-groups",
Method: protocol.MethodConfigGet,
},
)
getRes := readConfigPatchResponse(t, responses)
if !getRes.OK {
t.Fatalf("config.get failed: %#v", getRes.Error)
}
var payload struct {
Config struct {
Tools struct {
ShellDenyGroups map[string]bool `json:"shellDenyGroups"`
} `json:"tools"`
} `json:"config"`
}
rawPayload, err := json.Marshal(getRes.Payload)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(rawPayload, &payload); err != nil {
t.Fatal(err)
}
if v, ok := payload.Config.Tools.ShellDenyGroups["package_install"]; !ok || v {
t.Fatalf("config.get shellDenyGroups package_install = %v (ok=%v), want false", v, ok)
}
}
func readConfigPatchResponse(t *testing.T, responses <-chan []byte) protocol.ResponseFrame {
t.Helper()
select {
case raw := <-responses:
var res protocol.ResponseFrame
if err := json.Unmarshal(raw, &res); err != nil {
t.Fatal(err)
}
return res
case <-time.After(500 * time.Millisecond):
t.Fatal("timed out waiting for config.patch response")
return protocol.ResponseFrame{}
}
}