185f4f578e
- New credentials_test.go: 4 test functions covering credentialStore (new, save, lookup, update, persistence across restarts). - Extended handlers_test.go: fakeCore now supports WriteFact, Presence, RecentFacts/Nudges/Notes, RevertFact. Added 7 new test functions: TestNoCache, TestHandleSignal (5 subtestcases covering method guard, nil core, unknown key, known key, write error), TestHandleDash (3), TestHandleHistory (3), TestHandleRevert (6), and ListToolsError 502. - Fixed history.html: Go html/template requires conditional class rendered as separate <tr> branches, not inline attribute.
646 lines
19 KiB
Go
646 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/webauthn"
|
|
)
|
|
|
|
// fakeCore records the mutating calls handleTools makes and returns canned
|
|
// tool lists / errors. Embedding ipc.CoreAPI (nil) satisfies the large
|
|
// interface — only the methods the handlers touch are overridden; any other
|
|
// call would nil-panic, which is fine since the handlers never make them.
|
|
type fakeCore struct {
|
|
ipc.CoreAPI
|
|
|
|
proposed, enabled []ipc.Tool
|
|
listErr error
|
|
|
|
enableErr error
|
|
disableErr error
|
|
|
|
// recorded args from the last Enable/Disable call
|
|
gotEnableName string
|
|
gotEnableCmd []string
|
|
gotEnableDest bool
|
|
gotDisable string
|
|
|
|
// for handleSignal tests
|
|
writeLog []ipc.WriteFactReq
|
|
writeErr error
|
|
signalErr error
|
|
|
|
// for handleDash tests
|
|
presence ipc.Presence
|
|
facts []ipc.Fact
|
|
nudges []ipc.Nudge
|
|
notes []ipc.Note
|
|
dashErr error
|
|
|
|
// for handleRevert tests
|
|
revertKey string
|
|
revertNewID int64
|
|
revertErr error
|
|
|
|
// for handleHistory tests
|
|
historyFacts []ipc.Fact
|
|
historyErr error
|
|
}
|
|
|
|
func (f *fakeCore) EnableTool(_ context.Context, name string, cmd []string, destructive bool, _ time.Time) error {
|
|
f.gotEnableName, f.gotEnableCmd, f.gotEnableDest = name, cmd, destructive
|
|
return f.enableErr
|
|
}
|
|
|
|
func (f *fakeCore) DisableTool(_ context.Context, name string) error {
|
|
f.gotDisable = name
|
|
return f.disableErr
|
|
}
|
|
|
|
func (f *fakeCore) ListTools(_ context.Context, status string) ([]ipc.Tool, error) {
|
|
if f.listErr != nil {
|
|
return nil, f.listErr
|
|
}
|
|
switch status {
|
|
case "proposed":
|
|
return f.proposed, nil
|
|
default:
|
|
return f.enabled, nil
|
|
}
|
|
}
|
|
|
|
func (f *fakeCore) WriteFact(_ context.Context, req ipc.WriteFactReq) (int64, error) {
|
|
if f.writeErr != nil {
|
|
return 0, f.writeErr
|
|
}
|
|
if f.writeLog == nil {
|
|
f.writeLog = make([]ipc.WriteFactReq, 0)
|
|
}
|
|
f.writeLog = append(f.writeLog, req)
|
|
return int64(len(f.writeLog)), nil
|
|
}
|
|
|
|
func (f *fakeCore) Presence(_ context.Context) (ipc.Presence, error) {
|
|
if f.dashErr != nil {
|
|
return ipc.Presence{}, f.dashErr
|
|
}
|
|
return f.presence, nil
|
|
}
|
|
|
|
func (f *fakeCore) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
|
|
if f.historyErr != nil {
|
|
return nil, f.historyErr
|
|
}
|
|
if f.historyFacts != nil {
|
|
return f.historyFacts, nil
|
|
}
|
|
if f.dashErr != nil {
|
|
return nil, f.dashErr
|
|
}
|
|
return f.facts, nil
|
|
}
|
|
|
|
func (f *fakeCore) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
|
|
if f.dashErr != nil {
|
|
return nil, f.dashErr
|
|
}
|
|
return f.nudges, nil
|
|
}
|
|
|
|
func (f *fakeCore) RecentNotes(_ context.Context, _ int) ([]ipc.Note, error) {
|
|
if f.dashErr != nil {
|
|
return nil, f.dashErr
|
|
}
|
|
return f.notes, nil
|
|
}
|
|
|
|
func (f *fakeCore) RevertFact(_ context.Context, _ string) (int64, error) {
|
|
if f.revertErr != nil {
|
|
return 0, f.revertErr
|
|
}
|
|
return f.revertNewID, nil
|
|
}
|
|
|
|
// --- GET ---
|
|
|
|
func TestHandleTools_GET_RendersAndEscapes(t *testing.T) {
|
|
core := &fakeCore{
|
|
proposed: []ipc.Tool{{Name: "<b>x", Utterance: "restart the <i>thing"}},
|
|
enabled: []ipc.Tool{{Name: "svc", Cmd: []string{"systemctl", "restart"}, Destructive: true}},
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), core)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rr.Code)
|
|
}
|
|
body := rr.Body.String()
|
|
// html/template must escape the untrusted (STT-sourced) tool name.
|
|
if strings.Contains(body, "<b>x") {
|
|
t.Errorf("tool name rendered unescaped in output")
|
|
}
|
|
if !strings.Contains(body, "<b>x") {
|
|
t.Errorf("expected escaped tool name <b>x in output")
|
|
}
|
|
if !strings.Contains(body, "svc") || !strings.Contains(body, "systemctl restart") {
|
|
t.Errorf("enabled tool not rendered: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_NilCore_503(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, httptest.NewRequest(http.MethodGet, "/tools", nil), nil)
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("status = %d, want 503", rr.Code)
|
|
}
|
|
}
|
|
|
|
// --- POST enable ---
|
|
|
|
func postForm(action string, vals url.Values) *http.Request {
|
|
vals.Set("action", action)
|
|
r := httptest.NewRequest(http.MethodPost, "/tools", strings.NewReader(vals.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
return r
|
|
}
|
|
|
|
func TestHandleTools_POST_Enable_HappyPath(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("enable", url.Values{
|
|
"name": {"svc"},
|
|
"cmd": {"systemctl restart nginx"},
|
|
"destructive": {"on"},
|
|
}), core)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
if core.gotEnableName != "svc" {
|
|
t.Errorf("name = %q, want svc", core.gotEnableName)
|
|
}
|
|
if want := []string{"systemctl", "restart", "nginx"}; !reflect.DeepEqual(core.gotEnableCmd, want) {
|
|
t.Errorf("cmd = %v, want %v", core.gotEnableCmd, want)
|
|
}
|
|
if !core.gotEnableDest {
|
|
t.Errorf("destructive = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_POST_Enable_MissingName_400(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("enable", url.Values{"cmd": {"systemctl restart"}}), core)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_POST_Enable_MissingCmd_400(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("enable", url.Values{"name": {"svc"}}), core)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_POST_Enable_CoreError_502(t *testing.T) {
|
|
core := &fakeCore{enableErr: ipc.ErrForbidden}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("enable", url.Values{
|
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
|
}), core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want 502", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_POST_UnknownAction_400(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("frobnicate", url.Values{"name": {"svc"}}), core)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", rr.Code)
|
|
}
|
|
}
|
|
|
|
// --- POST disable ---
|
|
|
|
func TestHandleTools_POST_Disable_HappyPath(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("disable", url.Values{"name": {"svc"}}), core)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
if core.gotDisable != "svc" {
|
|
t.Errorf("disabled name = %q, want svc", core.gotDisable)
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_POST_Disable_MissingName_400(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("disable", url.Values{}), core)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleTools_POST_Disable_CoreError_502(t *testing.T) {
|
|
core := &fakeCore{disableErr: ipc.ErrToolNotFound}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("disable", url.Values{"name": {"svc"}}), core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want 502", rr.Code)
|
|
}
|
|
}
|
|
|
|
// TestEnableTool_NoInProcessAuthGate documents CURRENT behavior: handleTools
|
|
// performs the boundary-moving EnableTool with no in-process authentication —
|
|
// there is no passkey/session check in the Go handler. A POST enable with no
|
|
// prior WebAuthn AssertFinish succeeds (reaches core.EnableTool). Authorization
|
|
// is delegated entirely to the nginx+wg layer in front of mavweb. Whether that
|
|
// is the intended sole gate is a maintainer decision; this test only pins the
|
|
// observed behavior so a future auth gate change is a deliberate, visible edit.
|
|
func TestEnableTool_NoInProcessAuthGate(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
// No passkey session, no cookie, no header — just the raw POST.
|
|
handleTools(rr, postForm("enable", url.Values{
|
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
|
}), core)
|
|
if rr.Code != http.StatusOK || core.gotEnableName != "svc" {
|
|
t.Fatalf("expected unauthenticated enable to reach core (status=%d, name=%q); "+
|
|
"if this now fails, an in-process auth gate was added", rr.Code, core.gotEnableName)
|
|
}
|
|
}
|
|
|
|
// --- webauthn handler wiring (contract level, not crypto) ---
|
|
|
|
func newTestPasskey(t *testing.T) *PasskeyHandle {
|
|
t.Helper()
|
|
f, err := os.CreateTemp(t.TempDir(), "passkeys-*.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f.Close()
|
|
pk, err := newPasskeyHandle(webauthn.Config{
|
|
Origin: "https://maven.example",
|
|
RPID: "maven.example",
|
|
RPName: "maven",
|
|
}, nil, f.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return pk
|
|
}
|
|
|
|
func TestWebAuthn_Finish_MethodGuards(t *testing.T) {
|
|
pk := newTestPasskey(t)
|
|
for _, tc := range []struct {
|
|
name string
|
|
h http.HandlerFunc
|
|
}{
|
|
{"register", pk.RegisterFinish},
|
|
{"assert", pk.AssertFinish},
|
|
} {
|
|
rr := httptest.NewRecorder()
|
|
tc.h(rr, httptest.NewRequest(http.MethodGet, "/x", nil))
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("%s finish GET = %d, want 405", tc.name, rr.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWebAuthn_Finish_MalformedJSON_400(t *testing.T) {
|
|
pk := newTestPasskey(t)
|
|
for _, tc := range []struct {
|
|
name string
|
|
h http.HandlerFunc
|
|
}{
|
|
{"register", pk.RegisterFinish},
|
|
{"assert", pk.AssertFinish},
|
|
} {
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/x", strings.NewReader("{not json"))
|
|
tc.h(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("%s finish malformed = %d, want 400", tc.name, rr.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWebAuthn_Begin_ReturnsChallenge(t *testing.T) {
|
|
pk := newTestPasskey(t)
|
|
for _, tc := range []struct {
|
|
name string
|
|
h http.HandlerFunc
|
|
}{
|
|
{"register", pk.RegisterBegin},
|
|
{"assert", pk.AssertBegin},
|
|
} {
|
|
rr := httptest.NewRecorder()
|
|
tc.h(rr, httptest.NewRequest(http.MethodGet, "/x", nil))
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("%s begin = %d, want 200", tc.name, rr.Code)
|
|
continue
|
|
}
|
|
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
|
|
t.Errorf("%s begin content-type = %q, want application/json", tc.name, ct)
|
|
}
|
|
if !strings.Contains(rr.Body.String(), `"challenge"`) {
|
|
t.Errorf("%s begin body missing challenge: %s", tc.name, rr.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- middleware ---
|
|
|
|
func TestNoCache(t *testing.T) {
|
|
t.Parallel()
|
|
rr := httptest.NewRecorder()
|
|
innerCalled := false
|
|
noCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
innerCalled = true
|
|
w.Write([]byte("ok"))
|
|
})).ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
if ct := rr.Header().Get("Cache-Control"); ct != "no-cache, no-store, must-revalidate" {
|
|
t.Errorf("Cache-Control = %q, want %q", ct, "no-cache, no-store, must-revalidate")
|
|
}
|
|
if !innerCalled {
|
|
t.Error("inner handler was not called")
|
|
}
|
|
if rr.Body.String() != "ok" {
|
|
t.Errorf("body = %q, want %q", rr.Body.String(), "ok")
|
|
}
|
|
}
|
|
|
|
// --- handleSignal ---
|
|
|
|
func TestHandleSignal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("GET returns 405", func(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleSignal(rr, httptest.NewRequest(http.MethodGet, "/api/signal", nil), &fakeCore{})
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want 405", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("nil core returns 503", func(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleSignal(rr, httptest.NewRequest(http.MethodPost, "/api/signal", nil), nil)
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status = %d, want 503", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("unknown key returns 400", func(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/signal?key=nonexistent", nil)
|
|
handleSignal(rr, req, core)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want 400", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("known key desk_active writes fact with correct params", func(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/signal?key=desk_active", nil)
|
|
handleSignal(rr, req, core)
|
|
if rr.Code != http.StatusNoContent {
|
|
t.Fatalf("status = %d, want 204", rr.Code)
|
|
}
|
|
if len(core.writeLog) != 1 {
|
|
t.Fatalf("writeLog calls = %d, want 1", len(core.writeLog))
|
|
}
|
|
reqF := core.writeLog[0]
|
|
if reqF.Source != "infer:hyprland" {
|
|
t.Errorf("source = %q, want %q", reqF.Source, "infer:hyprland")
|
|
}
|
|
if reqF.Value != `"active"` {
|
|
t.Errorf("value = %q, want %q", reqF.Value, `"active"`)
|
|
}
|
|
if reqF.Kind != "env" {
|
|
t.Errorf("kind = %q, want %q", reqF.Kind, "env")
|
|
}
|
|
if reqF.Confidence != 1.0 {
|
|
t.Errorf("confidence = %f, want 1.0", reqF.Confidence)
|
|
}
|
|
})
|
|
|
|
t.Run("core WriteFact error returns 502", func(t *testing.T) {
|
|
core := &fakeCore{writeErr: ipc.ErrForbidden}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/signal?key=desk_active", nil)
|
|
handleSignal(rr, req, core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Errorf("status = %d, want 502", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- handleDash ---
|
|
|
|
func TestHandleDash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("nil core returns 503", func(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleDash(rr, httptest.NewRequest(http.MethodGet, "/dash", nil), nil)
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status = %d, want 503", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("core API error returns 502", func(t *testing.T) {
|
|
core := &fakeCore{dashErr: ipc.ErrNoFact}
|
|
rr := httptest.NewRecorder()
|
|
handleDash(rr, httptest.NewRequest(http.MethodGet, "/dash", nil), core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Errorf("status = %d, want 502", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("renders template with data", func(t *testing.T) {
|
|
now := time.Now()
|
|
core := &fakeCore{
|
|
presence: ipc.Presence{Bucket: ipc.Present, Score: 0.75, Updated: now},
|
|
facts: []ipc.Fact{
|
|
{Kind: "env", Key: "test-key", Value: `"val"`, Source: "test", Confidence: 1.0},
|
|
},
|
|
nudges: []ipc.Nudge{
|
|
{Rule: "test-rule", Channel: "test-chan", Message: "hello", Outcome: "pending"},
|
|
},
|
|
notes: []ipc.Note{
|
|
{Text: "a note", Source: "user"},
|
|
},
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
handleDash(rr, httptest.NewRequest(http.MethodGet, "/dash", nil), core)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, "present") {
|
|
t.Error("rendered output missing presence bucket")
|
|
}
|
|
if !strings.Contains(body, "0.75") {
|
|
t.Error("rendered output missing presence score")
|
|
}
|
|
if !strings.Contains(body, "test-key") {
|
|
t.Error("rendered output missing fact key")
|
|
}
|
|
if !strings.Contains(body, "test-rule") {
|
|
t.Error("rendered output missing nudge rule")
|
|
}
|
|
if !strings.Contains(body, "a note") {
|
|
t.Error("rendered output missing note text")
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- handleHistory ---
|
|
|
|
func TestHandleHistory(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("nil core returns 503", func(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleHistory(rr, httptest.NewRequest(http.MethodGet, "/history", nil), nil)
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status = %d, want 503", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("core RecentFacts error returns 502", func(t *testing.T) {
|
|
core := &fakeCore{historyErr: ipc.ErrNoFact}
|
|
rr := httptest.NewRecorder()
|
|
handleHistory(rr, httptest.NewRequest(http.MethodGet, "/history", nil), core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Errorf("status = %d, want 502", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("renders template with facts", func(t *testing.T) {
|
|
core := &fakeCore{
|
|
historyFacts: []ipc.Fact{
|
|
{Kind: "self", Key: "hist-key", Value: `"hist-val"`, Source: "test", Confidence: 0.5},
|
|
},
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
handleHistory(rr, httptest.NewRequest(http.MethodGet, "/history", nil), core)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, "hist-key") {
|
|
t.Error("rendered output missing fact key")
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- handleRevert ---
|
|
|
|
func TestHandleRevert(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("GET returns 405", func(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleRevert(rr, httptest.NewRequest(http.MethodGet, "/api/revert", nil), &fakeCore{})
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want 405", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("nil core returns 503", func(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
handleRevert(rr, httptest.NewRequest(http.MethodPost, "/api/revert", nil), nil)
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status = %d, want 503", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("empty key returns 400", func(t *testing.T) {
|
|
core := &fakeCore{}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key="))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
handleRevert(rr, req, core)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want 400", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("core returns ErrNoFact returns 404", func(t *testing.T) {
|
|
core := &fakeCore{revertErr: ipc.ErrNoFact}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=missing"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
handleRevert(rr, req, core)
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want 404", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("core returns error returns 502", func(t *testing.T) {
|
|
core := &fakeCore{revertErr: ipc.ErrForbidden}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=somekey"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
handleRevert(rr, req, core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Errorf("status = %d, want 502", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("happy path returns JSON with reverted and new_id", func(t *testing.T) {
|
|
core := &fakeCore{revertNewID: 42}
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/revert", strings.NewReader("key=test-key"))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
handleRevert(rr, req, core)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rr.Code)
|
|
}
|
|
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", ct)
|
|
}
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, `"reverted":true`) {
|
|
t.Errorf("body missing reverted:true: %s", body)
|
|
}
|
|
if !strings.Contains(body, `"new_id":42`) {
|
|
t.Errorf("body missing new_id:42: %s", body)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- handleTools ListTools error ---
|
|
|
|
func TestHandleTools_ListToolsError_502(t *testing.T) {
|
|
t.Parallel()
|
|
core := &fakeCore{listErr: ipc.ErrForbidden}
|
|
rr := httptest.NewRecorder()
|
|
handleTools(rr, postForm("enable", url.Values{
|
|
"name": {"svc"}, "cmd": {"systemctl restart"},
|
|
}), core)
|
|
if rr.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want 502; body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|