Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

109 lines
4.6 KiB
Go

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)
}
}