Public source validation / validate (push) Failing after 3m8s
109 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|