Ctrl K
ComunidadGuías…

internal/rest/access_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package rest_test

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/go-chi/chi/v5"

	"github.com/sarathsp06/sparrow/internal/accessauth"
	"github.com/sarathsp06/sparrow/internal/rest"
	"github.com/sarathsp06/sparrow/pkg/access"
	"github.com/sarathsp06/sparrow/pkg/access/memstore"
)

// brokenStore fails every write with a message that must never reach clients.
type brokenStore struct{ access.Store }

func (brokenStore) CreateToken(context.Context, access.Token, []byte) error {
	return errors.New("pq: password authentication failed for user secret-db-user")
}

func accessRouter(t *testing.T, store access.Store) http.Handler {
	t.Helper()
	svc, err := accessauth.NewWithStore(store, "", nil)
	if err != nil {
		t.Fatal(err)
	}
	r := chi.NewRouter()
	rest.Mount(r, nil, rest.AccessDeps{Service: svc}, rest.AIDeps{}, nil)
	return r
}

func post(h http.Handler, path, body string) *httptest.ResponseRecorder {
	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	return rec
}

func TestAccessStoreErrorsAreNotLeaked(t *testing.T) {
	rec := post(accessRouter(t, brokenStore{memstore.New()}), "/v1/tokens", `{"name":"ci"}`)
	if rec.Code != http.StatusInternalServerError {
		t.Fatalf("status = %d, want 500", rec.Code)
	}
	if strings.Contains(rec.Body.String(), "secret-db-user") || strings.Contains(rec.Body.String(), "pq:") {
		t.Fatalf("store error leaked to client: %s", rec.Body)
	}
}

func TestAccessRejectsUnsafeConsumerNames(t *testing.T) {
	h := accessRouter(t, memstore.New())
	for _, body := range []string{
		`{"name":"x","consumer":"../../v1"}`,
		`{"name":"x","consumer":"a/b"}`,
		`{"name":"x","consumer":" acme"}`,
	} {
		if rec := post(h, "/v1/tokens", body); rec.Code != http.StatusBadRequest {
			t.Errorf("POST /v1/tokens %s = %d, want 400", body, rec.Code)
		}
		if rec := post(h, "/v1/invites", body); rec.Code != http.StatusBadRequest {
			t.Errorf("POST /v1/invites %s = %d, want 400", body, rec.Code)
		}
	}
	if rec := post(h, "/v1/tokens", `{"name":"x","consumer":"acme"}`); rec.Code != http.StatusCreated {
		t.Fatalf("valid consumer = %d %s", rec.Code, rec.Body)
	}
}

func TestInvitePathsPointAtConsoleOrPortal(t *testing.T) {
	h := accessRouter(t, memstore.New())
	if rec := post(h, "/v1/invites", `{"name":"alice"}`); rec.Code != http.StatusCreated || !strings.Contains(rec.Body.String(), `"path":"/#invite=sparrow_inv_`) {
		t.Fatalf("console invite = %d %s", rec.Code, rec.Body)
	}
	if rec := post(h, "/v1/invites", `{"name":"acme","consumer":"acme"}`); rec.Code != http.StatusCreated || !strings.Contains(rec.Body.String(), `"path":"/portal#invite=sparrow_inv_`) {
		t.Fatalf("consumer invite = %d %s", rec.Code, rec.Body)
	}
	if rec := post(h, "/v1/invites", `{"name":"x","ttl_seconds":`+strconv.Itoa(8*24*3600)+`}`); rec.Code != http.StatusBadRequest {
		t.Fatalf("invite above 7 days = %d", rec.Code)
	}
}

func TestTenantTokenLifetimeDefaultsAndNeverExpires(t *testing.T) {
	svc, err := accessauth.NewWithStore(memstore.New(), "", nil)
	if err != nil {
		t.Fatal(err)
	}
	r := chi.NewRouter()
	rest.Mount(r, nil, rest.AccessDeps{Service: svc, TokenDefaultTTL: accessauth.TenantTokenDefaultTTL}, rest.AIDeps{}, nil)

	var out struct {
		Token struct {
			ExpiresAt *time.Time `json:"expires_at"`
		} `json:"token"`
	}
	decode := func(rec *httptest.ResponseRecorder) {
		t.Helper()
		out.Token.ExpiresAt = nil
		if rec.Code != http.StatusCreated {
			t.Fatalf("status %d: %s", rec.Code, rec.Body)
		}
		if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
			t.Fatal(err)
		}
	}

	decode(post(r, "/v1/tokens", `{"name":"default"}`))
	if out.Token.ExpiresAt == nil || time.Until(*out.Token.ExpiresAt) < 89*24*time.Hour {
		t.Fatalf("default tenant token should expire in ~90 days, got %v", out.Token.ExpiresAt)
	}
	decode(post(r, "/v1/tokens", `{"name":"forever","never_expires":true}`))
	if out.Token.ExpiresAt != nil {
		t.Fatalf("never_expires token has expires_at %v", out.Token.ExpiresAt)
	}
	for _, body := range []string{
		`{"name":"x","never_expires":true,"ttl_seconds":60}`,
		`{"name":"x","consumer":"acme","never_expires":true}`,
	} {
		if rec := post(r, "/v1/tokens", body); rec.Code != http.StatusBadRequest {
			t.Errorf("POST /v1/tokens %s = %d, want 400", body, rec.Code)
		}
	}
	if rec := post(r, "/v1/invites", `{"name":"ops","token_never_expires":true}`); rec.Code != http.StatusCreated || !strings.Contains(rec.Body.String(), `"token_ttl_seconds":null`) {
		t.Fatalf("invite with token_never_expires = %d %s", rec.Code, rec.Body)
	}
}