Pulse/internal/api/authorization_test.go
2026-03-18 16:06:30 +00:00

147 lines
4.7 KiB
Go

package api
import (
"errors"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockOrgLoader struct {
mock.Mock
}
func (m *mockOrgLoader) GetOrganization(orgID string) (*models.Organization, error) {
args := m.Called(orgID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.Organization), args.Error(1)
}
func TestDefaultAuthorizationChecker_TokenCanAccessOrg(t *testing.T) {
checker := NewAuthorizationChecker(nil)
t.Run("nil token", func(t *testing.T) {
assert.True(t, checker.TokenCanAccessOrg(nil, "any"))
})
t.Run("valid access single", func(t *testing.T) {
token := &config.APITokenRecord{OrgID: "acme"}
assert.True(t, checker.TokenCanAccessOrg(token, "acme"))
})
t.Run("valid access multi", func(t *testing.T) {
token := &config.APITokenRecord{OrgIDs: []string{"acme", "other"}}
assert.True(t, checker.TokenCanAccessOrg(token, "acme"))
})
t.Run("denied access", func(t *testing.T) {
token := &config.APITokenRecord{OrgID: "other"}
assert.False(t, checker.TokenCanAccessOrg(token, "acme"))
})
t.Run("wildcard legacy access denied", func(t *testing.T) {
token := &config.APITokenRecord{} // empty orgs = legacy/unbound
assert.False(t, checker.TokenCanAccessOrg(token, "tenant1"))
assert.False(t, checker.TokenCanAccessOrg(token, "default"))
})
}
func TestDefaultAuthorizationChecker_UserCanAccessOrg(t *testing.T) {
ml := new(mockOrgLoader)
checker := NewAuthorizationChecker(ml)
t.Run("default org allowed without metadata", func(t *testing.T) {
// Default org is always accessible to any authenticated user.
assert.True(t, checker.UserCanAccessOrg("user1", "default"))
})
t.Run("default org accessible regardless of membership", func(t *testing.T) {
// Default org is always accessible, even if membership data exists.
assert.True(t, checker.UserCanAccessOrg("user1", "default"))
assert.True(t, checker.UserCanAccessOrg("other", "default"))
})
t.Run("missing loader", func(t *testing.T) {
badChecker := NewAuthorizationChecker(nil)
// Default org is always accessible, even without a loader.
assert.True(t, badChecker.UserCanAccessOrg("user1", "default"))
// Non-default orgs are denied without a loader (fail closed).
assert.False(t, badChecker.UserCanAccessOrg("user1", "acme"))
})
t.Run("authorized member", func(t *testing.T) {
org := &models.Organization{
ID: "acme",
Members: []models.OrganizationMember{
{UserID: "user1", Role: models.OrgRoleAdmin},
},
}
ml.On("GetOrganization", "acme").Return(org, nil).Once()
assert.True(t, checker.UserCanAccessOrg("user1", "acme"))
})
t.Run("unauthorized user", func(t *testing.T) {
org := &models.Organization{
ID: "acme",
Members: []models.OrganizationMember{
{UserID: "other", Role: models.OrgRoleViewer},
},
}
ml.On("GetOrganization", "acme").Return(org, nil).Once()
assert.False(t, checker.UserCanAccessOrg("user1", "acme"))
})
t.Run("loader error", func(t *testing.T) {
ml.On("GetOrganization", "fail").Return(nil, errors.New("db error")).Once()
assert.False(t, checker.UserCanAccessOrg("user1", "fail"))
})
t.Run("not found", func(t *testing.T) {
ml.On("GetOrganization", "missing").Return(nil, nil).Once()
assert.False(t, checker.UserCanAccessOrg("user1", "missing"))
})
}
func TestDefaultAuthorizationChecker_CheckAccess(t *testing.T) {
ml := new(mockOrgLoader)
checker := NewAuthorizationChecker(ml)
t.Run("token takes precedence", func(t *testing.T) {
token := &config.APITokenRecord{OrgID: "acme"}
res := checker.CheckAccess(token, "user1", "acme")
assert.True(t, res.Allowed)
tokenLegacy := &config.APITokenRecord{OrgID: ""} // Legacy/unbound
res = checker.CheckAccess(tokenLegacy, "user1", "acme")
assert.False(t, res.Allowed)
tokenDenied := &config.APITokenRecord{OrgID: "other"}
res = checker.CheckAccess(tokenDenied, "user1", "acme")
assert.False(t, res.Allowed)
assert.Equal(t, "Token is not authorized for this organization", res.Reason)
})
t.Run("user fallback", func(t *testing.T) {
org := &models.Organization{
ID: "acme",
Members: []models.OrganizationMember{
{UserID: "user1", Role: models.OrgRoleAdmin},
},
}
ml.On("GetOrganization", "acme").Return(org, nil).Once()
res := checker.CheckAccess(nil, "user1", "acme")
assert.True(t, res.Allowed)
assert.Equal(t, "User is a member of the organization", res.Reason)
})
t.Run("no context", func(t *testing.T) {
res := checker.CheckAccess(nil, "", "acme")
assert.False(t, res.Allowed)
assert.Equal(t, "No authentication context provided", res.Reason)
})
}