201 lines
6.7 KiB
Go
201 lines
6.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/alexedwards/scs/v2"
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestBlocksLastAdminDemote(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
currentRole string
|
|
newRole string
|
|
activeAdminCount int64
|
|
want bool
|
|
}{
|
|
{name: "demote_last_admin", currentRole: "admin", newRole: "member", activeAdminCount: 1, want: true},
|
|
{name: "demote_zero_admins", currentRole: "admin", newRole: "member", activeAdminCount: 0, want: true},
|
|
{name: "demote_with_other_admins", currentRole: "admin", newRole: "member", activeAdminCount: 2, want: false},
|
|
{name: "promote_member", currentRole: "member", newRole: "admin", activeAdminCount: 1, want: false},
|
|
{name: "noop_admin", currentRole: "admin", newRole: "admin", activeAdminCount: 1, want: false},
|
|
{name: "noop_member", currentRole: "member", newRole: "member", activeAdminCount: 0, want: false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := blocksLastAdminDemote(tc.currentRole, tc.newRole, tc.activeAdminCount)
|
|
if got != tc.want {
|
|
t.Fatalf("blocksLastAdminDemote(%q,%q,%d)=%v want %v",
|
|
tc.currentRole, tc.newRole, tc.activeAdminCount, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBlocksLastAdminRemove(t *testing.T) {
|
|
t.Parallel()
|
|
if !blocksLastAdminRemove(1) {
|
|
t.Fatal("expected last admin remove blocked")
|
|
}
|
|
if !blocksLastAdminRemove(0) {
|
|
t.Fatal("expected zero admins remove blocked")
|
|
}
|
|
if blocksLastAdminRemove(2) {
|
|
t.Fatal("expected remove allowed when other admins remain")
|
|
}
|
|
}
|
|
|
|
func TestUpdateMemberRoleRejectsInvalidRole(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{}
|
|
cid := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
|
uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
|
ctx = context.WithValue(ctx, ctxCompanyID, cid)
|
|
ctx = context.WithValue(ctx, ctxRole, "admin")
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("userID", uid.String())
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/team/"+uid.String(), bytes.NewBufferString(`{"role":"owner"}`))
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
s.handleUpdateMemberRole(rec, req)
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d body=%s, want 400", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "invalid role") {
|
|
t.Fatalf("body = %s, want invalid role", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdateMemberRoleForbiddenForMember(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{}
|
|
cid := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
|
uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
|
ctx = context.WithValue(ctx, ctxCompanyID, cid)
|
|
ctx = context.WithValue(ctx, ctxRole, "member")
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/api/team/"+uid.String(), bytes.NewBufferString(`{"role":"admin"}`))
|
|
req = req.WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
s.handleUpdateMemberRole(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d body=%s, want 403", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAllowCompanyAdminOrPlatform(t *testing.T) {
|
|
t.Parallel()
|
|
uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
|
|
|
t.Run("company_admin", func(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{}
|
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
|
ctx = context.WithValue(ctx, ctxRole, "admin")
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
if !s.allowCompanyAdminOrPlatform(rec, req) {
|
|
t.Fatal("company admin should be allowed")
|
|
}
|
|
})
|
|
|
|
t.Run("member_denied", func(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{
|
|
testPlatformAdmin: func(context.Context, uuid.UUID) (bool, error) {
|
|
return false, nil
|
|
},
|
|
}
|
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
|
ctx = context.WithValue(ctx, ctxRole, "member")
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
if s.allowCompanyAdminOrPlatform(rec, req) {
|
|
t.Fatal("member without platform admin must be denied")
|
|
}
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want 403", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("platform_admin_member_role", func(t *testing.T) {
|
|
t.Parallel()
|
|
s := &Server{
|
|
testPlatformAdmin: func(_ context.Context, got uuid.UUID) (bool, error) {
|
|
if got != uid {
|
|
t.Fatalf("userID = %s, want %s", got, uid)
|
|
}
|
|
return true, nil
|
|
},
|
|
}
|
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
|
ctx = context.WithValue(ctx, ctxRole, "member")
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
|
|
rec := httptest.NewRecorder()
|
|
if !s.allowCompanyAdminOrPlatform(rec, req) {
|
|
t.Fatal("platform admin with membership role=member must be allowed for cutover")
|
|
}
|
|
})
|
|
|
|
t.Run("impersonated_member_denied", func(t *testing.T) {
|
|
t.Parallel()
|
|
actor := uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
|
|
sm := scs.New()
|
|
s := &Server{
|
|
Config: config.Config{AppEnv: "development"},
|
|
Sessions: sm,
|
|
testPlatformAdmin: func(_ context.Context, got uuid.UUID) (bool, error) {
|
|
return got == actor, nil
|
|
},
|
|
}
|
|
|
|
var token string
|
|
seed := LoadSession(sm)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
sm.Put(r.Context(), auth.SessionUserIDKey, uid.String())
|
|
sm.Put(r.Context(), auth.SessionImpersonatorIDKey, actor.String())
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
seedRec := httptest.NewRecorder()
|
|
seed.ServeHTTP(seedRec, httptest.NewRequest(http.MethodGet, "/seed", nil))
|
|
for _, c := range seedRec.Result().Cookies() {
|
|
if c.Name == sm.Cookie.Name {
|
|
token = c.Value
|
|
}
|
|
}
|
|
if token == "" {
|
|
t.Fatal("expected session cookie")
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
LoadSession(sm)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := context.WithValue(r.Context(), ctxUserID, uid)
|
|
ctx = context.WithValue(ctx, ctxRole, "member")
|
|
if s.allowCompanyAdminOrPlatform(w, r.WithContext(ctx)) {
|
|
t.Fatal("impersonated member must not retain company-admin powers")
|
|
}
|
|
})).ServeHTTP(rec, func() *http.Request {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.AddCookie(&http.Cookie{Name: sm.Cookie.Name, Value: token})
|
|
return req
|
|
}())
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status=%d body=%s, want 403", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
}
|