Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
package inventoryapi
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"github.com/itworx/pulse/internal/auth"
"github.com/itworx/pulse/internal/inventory"
"github.com/itworx/pulse/internal/problem"
)
type Repository interface {
SearchEntities(context.Context, inventory.EntityFilter) ([]inventory.EntitySummary, error)
GetEntityDetail(context.Context, string) (inventory.EntityDetail, error)
}
type Handler struct{ Repository Repository }
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.Repository == nil {
fail(w, r, http.StatusServiceUnavailable, "INVENTORY_UNAVAILABLE", "Inventory is not configured.")
return
}
if _, ok := auth.PrincipalFromContext(r.Context()); !ok {
fail(w, r, http.StatusUnauthorized, "UNAUTHORIZED", "Authentication is required to read inventory.")
return
}
if r.Method != http.MethodGet {
fail(w, r, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only read-only inventory operations are supported.")
return
}
path := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/v1/entities"), "/")
if path == "" {
h.list(w, r)
return
}
parts := strings.Split(path, "/")
if len(parts) == 1 && parts[0] != "" {
h.detail(w, r, parts[0], false)
return
}
if len(parts) == 2 && parts[0] != "" && parts[1] == "relations" {
h.detail(w, r, parts[0], true)
return
}
fail(w, r, http.StatusNotFound, "NOT_FOUND", "Inventory route not found.")
}
func (h Handler) list(w http.ResponseWriter, r *http.Request) {
limit := 50
if raw := r.URL.Query().Get("limit"); raw != "" {
value, err := strconv.Atoi(raw)
if err != nil || value < 1 || value > 100 {
fail(w, r, http.StatusBadRequest, "INVALID_LIMIT", "The inventory limit must be between 1 and 100.")
return
}
limit = value
}
afterName, afterID, err := decodeCursor(r.URL.Query().Get("after"))
if err != nil {
fail(w, r, http.StatusBadRequest, "INVALID_CURSOR", "The inventory cursor is invalid.")
return
}
direction := r.URL.Query().Get("order")
if direction == "" {
direction = "asc"
}
if direction != "asc" && direction != "desc" {
fail(w, r, http.StatusBadRequest, "INVALID_SORT", "Inventory order must be asc or desc.")
return
}
filter := inventory.EntityFilter{Limit: limit, AfterName: afterName, AfterID: afterID, Search: strings.TrimSpace(r.URL.Query().Get("q")), EntityType: strings.TrimSpace(r.URL.Query().Get("type")), Status: strings.TrimSpace(r.URL.Query().Get("status")), Direction: direction}
items, err := h.Repository.SearchEntities(r.Context(), filter)
if err != nil {
repositoryFailure(w, r, err)
return
}
hasMore := len(items) > limit
if hasMore {
items = items[:limit]
}
next := ""
if hasMore && len(items) > 0 {
last := items[len(items)-1]
next = encodeCursor(last.DisplayName, last.ID)
}
writeJSON(w, map[string]any{"items": items, "nextCursor": next, "hasMore": hasMore, "filters": map[string]string{"q": filter.Search, "type": filter.EntityType, "status": filter.Status, "order": filter.Direction}})
}
func (h Handler) detail(w http.ResponseWriter, r *http.Request, id string, relationsOnly bool) {
detail, err := h.Repository.GetEntityDetail(r.Context(), id)
if err != nil {
if errors.Is(err, inventory.ErrNotFound) {
fail(w, r, http.StatusNotFound, "ENTITY_NOT_FOUND", "The inventory entity does not exist.")
return
}
repositoryFailure(w, r, err)
return
}
if relationsOnly {
writeJSON(w, map[string]any{"items": detail.Relations})
return
}
writeJSON(w, detail)
}
func encodeCursor(name, id string) string {
return base64.RawURLEncoding.EncodeToString([]byte(strings.ToLower(name) + "\x00" + id))
}
func decodeCursor(value string) (string, string, error) {
if value == "" {
return "", "", nil
}
decoded, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return "", "", err
}
parts := strings.Split(string(decoded), "\x00")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", strconv.ErrSyntax
}
return parts[0], parts[1], nil
}
func repositoryFailure(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return
}
fail(w, r, http.StatusServiceUnavailable, "INVENTORY_UNAVAILABLE", "Inventory could not be read safely.")
}
func fail(w http.ResponseWriter, r *http.Request, status int, code, detail string) {
problem.Write(w, r, status, code, http.StatusText(status), detail, nil)
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "private, no-store")
_ = json.NewEncoder(w).Encode(value)
}
+108
View File
@@ -0,0 +1,108 @@
package inventoryapi
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/itworx/pulse/internal/auth"
"github.com/itworx/pulse/internal/inventory"
)
type stubRepository struct {
items []inventory.EntitySummary
detail inventory.EntityDetail
filter inventory.EntityFilter
err error
}
func (s *stubRepository) SearchEntities(_ context.Context, f inventory.EntityFilter) ([]inventory.EntitySummary, error) {
s.filter = f
return s.items, s.err
}
func (s *stubRepository) GetEntityDetail(_ context.Context, _ string) (inventory.EntityDetail, error) {
return s.detail, s.err
}
func request(method, target string) *http.Request {
r := httptest.NewRequest(method, target, nil)
return r.WithContext(auth.WithPrincipal(r.Context(), auth.Principal{Subject: "viewer", Role: auth.RoleViewer}))
}
func seeded() *stubRepository {
at := time.Date(2026, 8, 12, 0, 0, 0, 0, time.UTC)
item := inventory.EntitySummary{ID: "entity-a", EntityType: "container", CanonicalName: "api", DisplayName: "Manual API", Status: "attention", FirstSeenAt: &at, FactCount: 2, OverrideCount: 1, SourceCount: 2}
return &stubRepository{items: []inventory.EntitySummary{item, {ID: "entity-b", DisplayName: "Next"}}, detail: inventory.EntityDetail{Entity: item, Aliases: []inventory.AliasView{}, Facts: []inventory.FactView{}, Overrides: []inventory.OverrideView{}, Effective: []inventory.EffectiveValue{}, Relations: []inventory.RelationView{{ID: "relation-a", PeerID: "entity-b", PeerName: "Database"}}}}
}
func TestListSupportsFiltersSortAndCursor(t *testing.T) {
repo := seeded()
res := httptest.NewRecorder()
Handler{Repository: repo}.ServeHTTP(res, request(http.MethodGet, "/api/v1/entities?limit=1&q=api&type=container&status=attention&order=desc"))
if res.Code != 200 || !strings.Contains(res.Body.String(), `"displayName":"Manual API"`) || !strings.Contains(res.Body.String(), `"hasMore":true`) {
t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
}
if repo.filter.Search != "api" || repo.filter.EntityType != "container" || repo.filter.Status != "attention" || repo.filter.Direction != "desc" {
t.Fatalf("filter=%+v", repo.filter)
}
if !strings.Contains(res.Body.String(), `"nextCursor":"`) {
t.Fatal("missing cursor")
}
}
func TestListRejectsUnsafeBounds(t *testing.T) {
for _, target := range []string{"/api/v1/entities?limit=101", "/api/v1/entities?order=random", "/api/v1/entities?after=bad!"} {
res := httptest.NewRecorder()
Handler{Repository: seeded()}.ServeHTTP(res, request(http.MethodGet, target))
if res.Code != 400 {
t.Fatalf("%s status=%d", target, res.Code)
}
}
}
func TestDetailContainsEffectiveProvenanceAndRelations(t *testing.T) {
repo := seeded()
repo.detail.Effective = []inventory.EffectiveValue{{FieldName: "displayName", Value: []byte(`"Manual API"`), Origin: "override"}}
res := httptest.NewRecorder()
Handler{Repository: repo}.ServeHTTP(res, request(http.MethodGet, "/api/v1/entities/entity-a"))
if res.Code != 200 || !strings.Contains(res.Body.String(), `"origin":"override"`) || !strings.Contains(res.Body.String(), `"relations":[`) {
t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
}
rel := httptest.NewRecorder()
Handler{Repository: repo}.ServeHTTP(rel, request(http.MethodGet, "/api/v1/entities/entity-a/relations"))
if rel.Code != 200 || !strings.Contains(rel.Body.String(), "Database") {
t.Fatalf("relations=%s", rel.Body.String())
}
}
func TestErrorsAreSafeAndReadOnly(t *testing.T) {
repo := seeded()
repo.err = errors.New("database secret detail")
res := httptest.NewRecorder()
Handler{Repository: repo}.ServeHTTP(res, request(http.MethodGet, "/api/v1/entities"))
if res.Code != 503 || strings.Contains(res.Body.String(), "secret") {
t.Fatalf("status=%d body=%s", res.Code, res.Body.String())
}
repo.err = inventory.ErrNotFound
res = httptest.NewRecorder()
Handler{Repository: repo}.ServeHTTP(res, request(http.MethodGet, "/api/v1/entities/missing"))
if res.Code != 404 {
t.Fatalf("not found=%d", res.Code)
}
res = httptest.NewRecorder()
Handler{Repository: seeded()}.ServeHTTP(res, request(http.MethodPost, "/api/v1/entities"))
if res.Code != 405 {
t.Fatalf("mutation=%d", res.Code)
}
}
func TestAuthenticationAndUnavailableRepository(t *testing.T) {
res := httptest.NewRecorder()
Handler{Repository: seeded()}.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/api/v1/entities", nil))
if res.Code != 401 {
t.Fatalf("anonymous=%d", res.Code)
}
res = httptest.NewRecorder()
Handler{}.ServeHTTP(res, request(http.MethodGet, "/api/v1/entities"))
if res.Code != 503 {
t.Fatalf("nil repo=%d", res.Code)
}
}