internal/rest/portal_test.go
Rama main · réplica de lectura del repositorio original.
package rest_test
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/sarathsp06/sparrow/internal/accessauth"
"github.com/sarathsp06/sparrow/internal/crypto"
"github.com/sarathsp06/sparrow/internal/rest"
"github.com/sarathsp06/sparrow/internal/tenant"
"github.com/sarathsp06/sparrow/pkg/access"
"github.com/sarathsp06/sparrow/pkg/access/memstore"
)
type mintedToken struct {
Token struct {
ID string `json:"id"`
} `json:"token"`
Secret string `json:"secret"`
Reused bool `json:"reused"`
PortalPath string `json:"portal_path"`
}
func mintToken(t *testing.T, h http.Handler, body string) mintedToken {
t.Helper()
rec := post(h, "/v1/tokens", body)
if rec.Code != http.StatusCreated {
t.Fatalf("POST /v1/tokens %s = %d %s", body, rec.Code, rec.Body)
}
var out mintedToken
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
return out
}
func sealedRouter(t *testing.T) (http.Handler, *access.Service) {
t.Helper()
cryptoSvc, err := crypto.NewService(make([]byte, 32))
if err != nil {
t.Fatal(err)
}
svc, err := accessauth.NewWithStore(memstore.New(), "", accessauth.Sealer(cryptoSvc))
if err != nil {
t.Fatal(err)
}
r := chi.NewRouter()
rest.Mount(r, nil, rest.AccessDeps{Service: svc}, rest.AIDeps{}, nil)
return r, svc
}
func TestConsumerTokenComesWithAPortalLink(t *testing.T) {
h, svc := sealedRouter(t)
out := mintToken(t, h, `{"name":"acme portal","consumer":"acme co"}`)
if want := "/portal#token=" + out.Secret + "&consumer=acme%20co&expires="; !strings.HasPrefix(out.PortalPath, want) {
t.Fatalf("portal_path = %q, want prefix %q", out.PortalPath, want)
}
p, err := svc.Authenticate(context.Background(), out.Secret)
if err != nil || p.FullAccess() || *p.Scope != "acme co" {
t.Fatalf("principal = %+v, %v", p, err)
}
if full := mintToken(t, h, `{"name":"ci"}`); full.PortalPath != "" {
t.Fatalf("tenant-wide token got a portal link: %q", full.PortalPath)
}
}
func TestExternalIDReturnsTheValidConsumerToken(t *testing.T) {
h, svc := sealedRouter(t)
first := mintToken(t, h, `{"name":"portal","consumer":"acme","external_id":"user-1"}`)
again := mintToken(t, h, `{"name":"portal","consumer":"acme","external_id":"user-1","ttl_seconds":60}`)
if first.Reused || !again.Reused || again.Secret != first.Secret || again.Token.ID != first.Token.ID || again.PortalPath != first.PortalPath {
t.Fatalf("same key: first %+v, again %+v; want the same token reused", first, again)
}
if other := mintToken(t, h, `{"name":"portal","consumer":"acme","external_id":"user-2"}`); other.Reused || other.Token.ID == first.Token.ID {
t.Fatalf("other key reused %+v", other)
}
if otherConsumer := mintToken(t, h, `{"name":"portal","consumer":"globex","external_id":"user-1"}`); otherConsumer.Reused {
t.Fatalf("key reused across consumers %+v", otherConsumer)
}
if plain := mintToken(t, h, `{"name":"portal","consumer":"acme"}`); plain.Reused || plain.Token.ID == first.Token.ID {
t.Fatalf("no key reused %+v", plain)
}
req := httptest.NewRequest(http.MethodDelete, "/v1/tokens/"+first.Token.ID, nil)
del := httptest.NewRecorder()
h.ServeHTTP(del, req)
if del.Code != http.StatusNoContent {
t.Fatalf("revoke = %d %s", del.Code, del.Body)
}
var authErr *access.AuthError
if _, err := svc.Authenticate(context.Background(), first.Secret); !errors.As(err, &authErr) || authErr.Reason != access.ReasonRevoked {
t.Fatalf("after revoke err = %v, want revoked", err)
}
if fresh := mintToken(t, h, `{"name":"portal","consumer":"acme","external_id":"user-1"}`); fresh.Reused || fresh.Token.ID == first.Token.ID {
t.Fatalf("after revoke %+v, want a new token", fresh)
}
}
func TestExternalIDRules(t *testing.T) {
h, _ := sealedRouter(t)
if rec := post(h, "/v1/tokens", `{"name":"ci","external_id":"k"}`); rec.Code != http.StatusBadRequest {
t.Fatalf("tenant-wide with key = %d %s, want 400", rec.Code, rec.Body)
}
if rec := post(accessRouter(t, memstore.New()), "/v1/tokens", `{"name":"p","consumer":"acme","external_id":"k"}`); rec.Code != http.StatusServiceUnavailable {
t.Fatalf("without sealer = %d %s, want 503", rec.Code, rec.Body)
}
}
// asTenant serves h for tenant id, as an embedder's server.WithTenant does.
func asTenant(h http.Handler, id uuid.UUID) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r.WithContext(tenant.WithTenant(r.Context(), id)))
})
}
func TestTokensBelongToTheTenantThatCreatedThem(t *testing.T) {
h, svc := sealedRouter(t)
idA, idB := uuid.New(), uuid.New()
tenantA, tenantB := asTenant(h, idA), asTenant(h, idB)
body := `{"name":"portal","consumer":"acme","external_id":"user-1"}`
a := mintToken(t, tenantA, body)
if p, err := svc.Authenticate(context.Background(), a.Secret); err != nil || p.Realm != idA.String() {
t.Fatalf("token minted on tenant A has principal %+v, %v; want realm %s", p, err, idA)
}
// external_id is per tenant: the same id on tenant B is another token.
b := mintToken(t, tenantB, body)
if b.Reused || b.Token.ID == a.Token.ID || b.Secret == a.Secret {
t.Fatalf("tenant B got tenant A's token for the same external_id: a %+v, b %+v", a, b)
}
if again := mintToken(t, tenantA, body); !again.Reused || again.Token.ID != a.Token.ID {
t.Fatalf("tenant A lost its token for user-1: %+v", again)
}
list := httptest.NewRecorder()
tenantB.ServeHTTP(list, httptest.NewRequest(http.MethodGet, "/v1/tokens", nil))
if list.Code != http.StatusOK || strings.Contains(list.Body.String(), a.Token.ID) || !strings.Contains(list.Body.String(), b.Token.ID) {
t.Fatalf("tenant B lists %d %s; want only its own token", list.Code, list.Body)
}
del := httptest.NewRecorder()
tenantB.ServeHTTP(del, httptest.NewRequest(http.MethodDelete, "/v1/tokens/"+a.Token.ID, nil))
if del.Code != http.StatusNotFound {
t.Fatalf("tenant B revoking tenant A's token = %d %s, want 404", del.Code, del.Body)
}
if _, err := svc.Authenticate(context.Background(), a.Secret); err != nil {
t.Fatalf("tenant A's token stopped working: %v", err)
}
}