package dashboardapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/itworx/pulse/internal/audit" "github.com/itworx/pulse/internal/auth" "github.com/itworx/pulse/internal/correlation" "github.com/itworx/pulse/internal/dashboard" ) func requestWithPrincipal(method, path, body string, principal *auth.Principal) *httptest.ResponseRecorder { request := httptest.NewRequest(method, path, strings.NewReader(body)) request.Header.Set("Content-Type", "application/json") request = request.WithContext(correlation.WithContext(request.Context(), "m3-03-test-correlation")) if principal != nil { request = request.WithContext(auth.WithPrincipal(request.Context(), *principal)) } response := httptest.NewRecorder() (Handler{Repository: dashboard.Repository{}}).ServeHTTP(response, request) return response } func problemCode(t *testing.T, response *httptest.ResponseRecorder) string { t.Helper() var body struct{ Code string } if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil { t.Fatalf("problem response is not JSON: %v", err) } return body.Code } func TestHandlerRequiresAuthenticationWithProblemResponse(t *testing.T) { response := requestWithPrincipal(http.MethodGet, "/api/v1/dashboards", "", nil) if response.Code != http.StatusUnauthorized { t.Fatalf("status=%d", response.Code) } if got := response.Header().Get("Content-Type"); got != "application/problem+json" { t.Fatalf("content type=%q", got) } if got := problemCode(t, response); got != "UNAUTHORIZED" { t.Fatalf("code=%q", got) } if got := response.Header().Get(correlation.Header); got != "m3-03-test-correlation" { t.Fatalf("correlation=%q", got) } } func TestHandlerEnforcesEditorForMutation(t *testing.T) { viewer := auth.Principal{Subject: "viewer", Role: auth.RoleViewer} response := requestWithPrincipal(http.MethodPost, "/api/v1/dashboards", "{}", &viewer) if response.Code != http.StatusForbidden { t.Fatalf("status=%d", response.Code) } if got := problemCode(t, response); got != "FORBIDDEN" { t.Fatalf("code=%q", got) } } func TestHandlerEnforcesEditorForPreview(t *testing.T) { viewer := auth.Principal{Subject: "viewer", Role: auth.RoleViewer} response := requestWithPrincipal(http.MethodPost, "/api/v1/dashboards/00000000-0000-0000-0000-000000000001/preview", `{"widget":{}}`, &viewer) if response.Code != http.StatusForbidden || problemCode(t, response) != "FORBIDDEN" { t.Fatalf("response=%d %s", response.Code, response.Body.String()) } } func TestHandlerRejectsInvalidContentTypeAndVersion(t *testing.T) { editor := auth.Principal{Subject: "editor", Role: auth.RoleEditor} request := httptest.NewRequest(http.MethodPost, "/api/v1/dashboards", strings.NewReader("{}")) request.Header.Set("Content-Type", "text/plain") request = request.WithContext(auth.WithPrincipal(context.Background(), editor)) response := httptest.NewRecorder() (Handler{Repository: dashboard.Repository{}}).ServeHTTP(response, request) if response.Code != http.StatusBadRequest || problemCode(t, response) != "INVALID_DOCUMENT" { t.Fatalf("response=%d %s", response.Code, response.Body.String()) } response = requestWithPrincipal(http.MethodPost, "/api/v1/dashboards/00000000-0000-0000-0000-000000000001/restore/not-a-number", "{}", &editor) if response.Code != http.StatusBadRequest || problemCode(t, response) != "INVALID_VERSION" { t.Fatalf("response=%d %s", response.Code, response.Body.String()) } } func TestHandlerRejectsUnsupportedMethod(t *testing.T) { viewer := auth.Principal{Subject: "viewer", Role: auth.RoleViewer} response := requestWithPrincipal(http.MethodPut, "/api/v1/dashboards", "{}", &viewer) if response.Code != http.StatusMethodNotAllowed { t.Fatalf("status=%d", response.Code) } } func TestHandlerRecordsAuditedSafeDiff(t *testing.T) { store := &audit.MemoryStore{} request := httptest.NewRequest(http.MethodPost, "/api/v1/dashboards", strings.NewReader("{}")) request = request.WithContext(correlation.WithContext(request.Context(), "audit-correlation")) handler := Handler{Audit: store} if err := handler.record(request, "subject-1", "dashboard.update_document", "00000000-0000-0000-0000-000000000001", map[string]any{"revision": 1}, map[string]any{"revision": 2}); err != nil { t.Fatal(err) } if len(store.Events) != 1 || store.Events[0].Action != "dashboard.update_document" || store.Events[0].CorrelationID != "audit-correlation" { t.Fatalf("events=%+v", store.Events) } if _, ok := store.Events[0].After["document"]; ok { t.Fatal("audit event unexpectedly contains full document") } }