mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-25 00:43:34 +00:00
140 lines
5.4 KiB
Go
140 lines
5.4 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
|
)
|
|
|
|
func TestMockActionInboxAndDetailUseCanonicalResponses(t *testing.T) {
|
|
previous := mock.IsMockEnabled()
|
|
if err := mock.SetEnabled(true); err != nil {
|
|
t.Fatalf("enable mock mode: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
|
handler := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
|
|
|
|
for _, test := range []struct {
|
|
view string
|
|
wantCount int
|
|
}{
|
|
{view: "pending", wantCount: 3},
|
|
{view: "settled", wantCount: 3},
|
|
} {
|
|
recorder := httptest.NewRecorder()
|
|
handler.HandleListActions(recorder, httptest.NewRequest(http.MethodGet, "/api/actions?view="+test.view, nil))
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("list %s status = %d, body=%s", test.view, recorder.Code, recorder.Body.String())
|
|
}
|
|
var response actionInboxResponse
|
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
|
t.Fatalf("decode %s response: %v", test.view, err)
|
|
}
|
|
if response.Count != test.wantCount || len(response.Actions) != test.wantCount {
|
|
t.Fatalf("list %s count = %d actions=%d, want %d", test.view, response.Count, len(response.Actions), test.wantCount)
|
|
}
|
|
if !response.ReadOnly {
|
|
t.Fatalf("list %s must declare the mock projection read-only", test.view)
|
|
}
|
|
for _, action := range response.Actions {
|
|
if action.Plan.PolicyDecision.DecisionID == "" || action.Plan.PlanHash == "" {
|
|
t.Fatalf("list %s returned incomplete review identity: %#v", test.view, action.Plan)
|
|
}
|
|
if action.Resource == nil || action.Resource.Name == "" || action.Resource.Type == "" {
|
|
t.Fatalf("list %s returned no canonical resource presentation: %#v", test.view, action.Resource)
|
|
}
|
|
}
|
|
}
|
|
|
|
fixtures := mock.ActionFixtures()
|
|
detailRecorder := httptest.NewRecorder()
|
|
detailRequest := httptest.NewRequest(http.MethodGet, "/api/actions/"+fixtures[0].Audit.ID, nil)
|
|
detailRequest.SetPathValue("id", fixtures[0].Audit.ID)
|
|
handler.HandleGetAction(detailRecorder, detailRequest)
|
|
if detailRecorder.Code != http.StatusOK {
|
|
t.Fatalf("detail status = %d, body=%s", detailRecorder.Code, detailRecorder.Body.String())
|
|
}
|
|
var detail actionDetailResponse
|
|
if err := json.NewDecoder(detailRecorder.Body).Decode(&detail); err != nil {
|
|
t.Fatalf("decode detail: %v", err)
|
|
}
|
|
if detail.Audit.ID != fixtures[0].Audit.ID || len(detail.Events) == 0 {
|
|
t.Fatalf("detail = %#v", detail)
|
|
}
|
|
if detail.Audit.Resource == nil || detail.Audit.Resource.Name == "" {
|
|
t.Fatalf("detail returned no canonical resource presentation: %#v", detail.Audit.Resource)
|
|
}
|
|
if !detail.ReadOnly {
|
|
t.Fatal("mock detail must declare itself read-only")
|
|
}
|
|
}
|
|
|
|
func TestMockActionInboxHonorsLimitAndLegacyPendingProjection(t *testing.T) {
|
|
previous := mock.IsMockEnabled()
|
|
if err := mock.SetEnabled(true); err != nil {
|
|
t.Fatalf("enable mock mode: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
|
handler := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
|
|
|
|
limited := httptest.NewRecorder()
|
|
handler.HandleListActions(limited, httptest.NewRequest(http.MethodGet, "/api/actions?view=pending&limit=1", nil))
|
|
var limitedResponse actionInboxResponse
|
|
if err := json.NewDecoder(limited.Body).Decode(&limitedResponse); err != nil {
|
|
t.Fatalf("decode limited response: %v", err)
|
|
}
|
|
if limitedResponse.Count != 1 || len(limitedResponse.Actions) != 1 {
|
|
t.Fatalf("limited response count=%d actions=%d", limitedResponse.Count, len(limitedResponse.Actions))
|
|
}
|
|
if !limitedResponse.ReadOnly {
|
|
t.Fatal("limited mock response must declare itself read-only")
|
|
}
|
|
|
|
pending := httptest.NewRecorder()
|
|
handler.HandleListPendingActions(pending, httptest.NewRequest(http.MethodGet, "/api/actions/pending", nil))
|
|
var pendingResponse pendingActionsResponse
|
|
if err := json.NewDecoder(pending.Body).Decode(&pendingResponse); err != nil {
|
|
t.Fatalf("decode pending response: %v", err)
|
|
}
|
|
if pendingResponse.Count != 1 || pendingResponse.Actions[0].State != "pending_approval" {
|
|
t.Fatalf("legacy pending response = %#v", pendingResponse)
|
|
}
|
|
if !pendingResponse.ReadOnly {
|
|
t.Fatal("legacy pending response must declare itself read-only")
|
|
}
|
|
}
|
|
|
|
func TestMockActionMutationsFailReadOnly(t *testing.T) {
|
|
previous := mock.IsMockEnabled()
|
|
if err := mock.SetEnabled(true); err != nil {
|
|
t.Fatalf("enable mock mode: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = mock.SetEnabled(previous) })
|
|
handler := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
body string
|
|
run func(http.ResponseWriter, *http.Request)
|
|
}{
|
|
{name: "plan", path: "/api/actions/plan", body: `{}`, run: handler.HandlePlanAction},
|
|
{name: "decide", path: "/api/actions/demo-action-install-updates/decision", body: `{"outcome":"approved"}`, run: handler.HandleDecideAction},
|
|
{name: "execute", path: "/api/actions/demo-action-restart-checkout/execute", body: `{}`, run: handler.HandleExecuteAction},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, test.path, bytes.NewBufferString(test.body))
|
|
test.run(recorder, request)
|
|
if recorder.Code != http.StatusForbidden || !bytes.Contains(recorder.Body.Bytes(), []byte("mock_mode_enabled")) {
|
|
t.Fatalf("status = %d, body=%s", recorder.Code, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|