mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
158 lines
4.6 KiB
Go
158 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
func TestUserLimitIsolation_IndependentPerOrg(t *testing.T) {
|
|
setMaxUsersLimitForTests(t, map[string]int64{
|
|
maxUsersLicenseGateKey: 5,
|
|
})
|
|
|
|
orgA := &models.Organization{
|
|
ID: "org-a",
|
|
Members: makeMembersForIsolationTest(5),
|
|
}
|
|
orgB := &models.Organization{
|
|
ID: "org-b",
|
|
Members: makeMembersForIsolationTest(2),
|
|
}
|
|
|
|
recA := httptest.NewRecorder()
|
|
blockedA := enforceUserLimitForMemberAdd(recA, context.Background(), orgA)
|
|
if !blockedA {
|
|
t.Fatalf("expected org A add to be blocked at limit")
|
|
}
|
|
if recA.Code != http.StatusPaymentRequired {
|
|
t.Fatalf("expected org A status %d, got %d", http.StatusPaymentRequired, recA.Code)
|
|
}
|
|
|
|
recB := httptest.NewRecorder()
|
|
blockedB := enforceUserLimitForMemberAdd(recB, context.Background(), orgB)
|
|
if blockedB {
|
|
t.Fatalf("expected org B add to be allowed under limit")
|
|
}
|
|
if recB.Code != http.StatusOK {
|
|
t.Fatalf("expected org B status %d, got %d", http.StatusOK, recB.Code)
|
|
}
|
|
}
|
|
|
|
func TestUserLimitIsolation_NoLimitAllowsUnlimited(t *testing.T) {
|
|
SetLicenseServiceProvider(nil)
|
|
t.Cleanup(func() {
|
|
SetLicenseServiceProvider(nil)
|
|
})
|
|
|
|
org := &models.Organization{
|
|
ID: "org-unlimited",
|
|
Members: makeMembersForIsolationTest(100),
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
blocked := enforceUserLimitForMemberAdd(rec, context.Background(), org)
|
|
if blocked {
|
|
t.Fatalf("expected add to be allowed without max_users limit")
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestUserLimitIsolation_ExistingMemberUpdateNotBlocked(t *testing.T) {
|
|
t.Setenv("PULSE_DEV", "true")
|
|
defer SetMultiTenantEnabled(false)
|
|
SetMultiTenantEnabled(true)
|
|
|
|
setMaxUsersLimitForTests(t, map[string]int64{
|
|
maxUsersLicenseGateKey: 5,
|
|
})
|
|
|
|
persistence := config.NewMultiTenantPersistence(t.TempDir())
|
|
h := NewOrgHandlers(persistence, nil)
|
|
|
|
createReq := withUser(
|
|
httptest.NewRequest(http.MethodPost, "/api/orgs", bytes.NewBufferString(`{"id":"acme","displayName":"Acme"}`)),
|
|
"alice",
|
|
)
|
|
createRec := httptest.NewRecorder()
|
|
h.HandleCreateOrg(createRec, createReq)
|
|
if createRec.Code != http.StatusCreated {
|
|
t.Fatalf("create failed: %d %s", createRec.Code, createRec.Body.String())
|
|
}
|
|
|
|
for _, userID := range []string{"bob", "charlie", "dave", "erin"} {
|
|
inviteReq := withUser(
|
|
httptest.NewRequest(http.MethodPost, "/api/orgs/acme/members", bytes.NewBufferString(`{"userId":"`+userID+`","role":"viewer"}`)),
|
|
"alice",
|
|
)
|
|
inviteReq.SetPathValue("id", "acme")
|
|
inviteRec := httptest.NewRecorder()
|
|
h.HandleInviteMember(inviteRec, inviteReq)
|
|
if inviteRec.Code != http.StatusOK {
|
|
t.Fatalf("invite %s failed: %d %s", userID, inviteRec.Code, inviteRec.Body.String())
|
|
}
|
|
}
|
|
|
|
orgBeforeUpdate, err := persistence.LoadOrganization("acme")
|
|
if err != nil {
|
|
t.Fatalf("load org before update failed: %v", err)
|
|
}
|
|
if got := currentUserCount(orgBeforeUpdate); got != 5 {
|
|
t.Fatalf("expected currentUserCount=5 before role update, got %d", got)
|
|
}
|
|
|
|
updateReq := withUser(
|
|
httptest.NewRequest(http.MethodPost, "/api/orgs/acme/members", bytes.NewBufferString(`{"userId":"bob","role":"admin"}`)),
|
|
"alice",
|
|
)
|
|
updateReq.SetPathValue("id", "acme")
|
|
updateRec := httptest.NewRecorder()
|
|
h.HandleInviteMember(updateRec, updateReq)
|
|
if updateRec.Code != http.StatusOK {
|
|
t.Fatalf("expected existing member update to bypass add-limit check, got %d: %s", updateRec.Code, updateRec.Body.String())
|
|
}
|
|
|
|
orgAfterUpdate, err := persistence.LoadOrganization("acme")
|
|
if err != nil {
|
|
t.Fatalf("load org after update failed: %v", err)
|
|
}
|
|
if got := currentUserCount(orgAfterUpdate); got != 5 {
|
|
t.Fatalf("expected member count to remain 5 after role update, got %d", got)
|
|
}
|
|
|
|
member, ok := findMemberForIsolationTest(orgAfterUpdate, "bob")
|
|
if !ok {
|
|
t.Fatalf("expected bob to remain a member after role update")
|
|
}
|
|
if member.Role != models.OrgRoleAdmin {
|
|
t.Fatalf("expected bob role %q, got %q", models.OrgRoleAdmin, member.Role)
|
|
}
|
|
}
|
|
|
|
func makeMembersForIsolationTest(count int) []models.OrganizationMember {
|
|
members := make([]models.OrganizationMember, count)
|
|
for i := 0; i < count; i++ {
|
|
members[i] = models.OrganizationMember{
|
|
UserID: "user-" + strconv.Itoa(i+1),
|
|
Role: models.OrgRoleViewer,
|
|
}
|
|
}
|
|
return members
|
|
}
|
|
|
|
func findMemberForIsolationTest(org *models.Organization, userID string) (models.OrganizationMember, bool) {
|
|
for _, member := range org.Members {
|
|
if member.UserID == userID {
|
|
return member, true
|
|
}
|
|
}
|
|
return models.OrganizationMember{}, false
|
|
}
|