package scopes
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExpandScopes(t *testing.T) {
tests := []struct {
name string
required []Scope
expected []string
}{
{
name: "nil returns nil",
required: nil,
expected: nil,
},
{
name: "empty returns nil",
required: []Scope{},
expected: nil,
},
{
name: "repo scope returns just repo",
required: []Scope{Repo},
expected: []string{"repo"},
},
{
name: "public_repo also accepts repo (parent)",
required: []Scope{PublicRepo},
expected: []string{"public_repo", "repo"},
},
{
name: "security_events also accepts repo (parent)",
required: []Scope{SecurityEvents},
expected: []string{"repo", "security_events"},
},
{
name: "read:org also accepts write:org and admin:org (parents)",
required: []Scope{ReadOrg},
expected: []string{"admin:org", "read:org", "write:org"},
},
{
name: "write:org also accepts admin:org (parent)",
required: []Scope{WriteOrg},
expected: []string{"admin:org", "write:org"},
},
{
name: "admin:org returns just admin:org (no parent)",
required: []Scope{AdminOrg},
expected: []string{"admin:org"},
},
{
name: "read:project also accepts project (parent)",
required: []Scope{ReadProject},
expected: []string{"project", "read:project"},
},
{
name: "project returns just project (no parent)",
required: []Scope{Project},
expected: []string{"project"},
},
{
name: "gist returns just gist (no parent)",
required: []Scope{Gist},
expected: []string{"gist"},
},
{
name: "notifications returns just notifications (no parent)",
required: []Scope{Notifications},
expected: []string{"notifications"},
},
{
name: "read:packages also accepts write:packages (parent)",
required: []Scope{ReadPackages},
expected: []string{"read:packages", "write:packages"},
},
{
name: "read:user also accepts user (parent)",
required: []Scope{ReadUser},
expected: []string{"read:user", "user"},
},
{
name: "multiple scopes combine correctly",
required: []Scope{PublicRepo, ReadOrg},
expected: []string{"admin:org", "public_repo", "read:org", "repo", "write:org"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExpandScopes(tt.required...)
// Sort both for consistent comparison
if result != nil {
sort.Strings(result)
}
if tt.expected != nil {
sort.Strings(tt.expected)
}
assert.Equal(t, tt.expected, result)
})
}
}
func TestToStringSlice(t *testing.T) {
tests := []struct {
name string
scopes []Scope
expected []string
}{
{
name: "empty returns empty",
scopes: []Scope{},
expected: []string{},
},
{
name: "single scope",
scopes: []Scope{Repo},
expected: []string{"repo"},
},
{
name: "multiple scopes",
scopes: []Scope{Repo, Gist, ReadOrg},
expected: []string{"repo", "gist", "read:org"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ToStringSlice(tt.scopes...)
assert.Equal(t, tt.expected, result)
})
}
}
func TestScopeHierarchy(t *testing.T) {
// Verify the hierarchy is correctly defined
assert.Contains(t, ScopeHierarchy[Repo], PublicRepo)
assert.Contains(t, ScopeHierarchy[Repo], SecurityEvents)
assert.Contains(t, ScopeHierarchy[AdminOrg], WriteOrg)
assert.Contains(t, ScopeHierarchy[AdminOrg], ReadOrg)
assert.Contains(t, ScopeHierarchy[WriteOrg], ReadOrg)
assert.Contains(t, ScopeHierarchy[Project], ReadProject)
assert.Contains(t, ScopeHierarchy[WritePackages], ReadPackages)
assert.Contains(t, ScopeHierarchy[User], ReadUser)
assert.Contains(t, ScopeHierarchy[User], UserEmail)
}
func TestExpandScopeSet(t *testing.T) {
tests := []struct {
name string
scopes []string
expected map[string]bool
}{
{
name: "empty scopes",
scopes: []string{},
expected: map[string]bool{},
},
{
name: "repo expands to include public_repo and security_events",
scopes: []string{"repo"},
expected: map[string]bool{
"repo": true,
"public_repo": true,
"security_events": true,
},
},
{
name: "admin:org expands to include write:org and read:org",
scopes: []string{"admin:org"},
expected: map[string]bool{
"admin:org": true,
"write:org": true,
"read:org": true,
},
},
{
name: "write:org expands to include read:org",
scopes: []string{"write:org"},
expected: map[string]bool{
"write:org": true,
"read:org": true,
},
},
{
name: "user expands to include read:user and user:email",
scopes: []string{"user"},
expected: map[string]bool{
"user": true,
"read:user": true,
"user:email": true,
},
},
{
name: "scope without children stays as-is",
scopes: []string{"gist"},
expected: map[string]bool{
"gist": true,
},
},
{
name: "multiple scopes combine correctly",
scopes: []string{"repo", "gist"},
expected: map[string]bool{
"repo": true,
"public_repo": true,
"security_events": true,
"gist": true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := expandScopeSet(tt.scopes)
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasRequiredScopes(t *testing.T) {
tests := []struct {
name string
tokenScopes []string
acceptedScopes []string
expected bool
}{
{
name: "no accepted scopes - always allowed",
tokenScopes: []string{},
acceptedScopes: []string{},
expected: true,
},
{
name: "nil accepted scopes - always allowed",
tokenScopes: []string{"repo"},
acceptedScopes: nil,
expected: true,
},
{
name: "token has exact required scope",
tokenScopes: []string{"repo"},
acceptedScopes: []string{"repo"},
expected: true,
},
{
name: "token has parent scope that grants access",
tokenScopes: []string{"repo"},
acceptedScopes: []string{"public_repo"},
expected: true,
},
{
name: "token has parent scope for security_events",
tokenScopes: []string{"repo"},
acceptedScopes: []string{"security_events"},
expected: true,
},
{
name: "token has admin:org which grants read:org",
tokenScopes: []string{"admin:org"},
acceptedScopes: []string{"read:org"},
expected: true,
},
{
name: "token has write:org which grants read:org",
tokenScopes: []string{"write:org"},
acceptedScopes: []string{"read:org"},
expected: true,
},
{
name: "token missing required scope",
tokenScopes: []string{"gist"},
acceptedScopes: []string{"repo"},
expected: false,
},
{
name: "token has child but not parent - fails",
tokenScopes: []string{"public_repo"},
acceptedScopes: []string{"repo"},
expected: false,
},
{
name: "multiple token scopes - one matches",
tokenScopes: []string{"gist", "repo"},
acceptedScopes: []string{"public_repo"},
expected: true,
},
{
name: "multiple accepted scopes - token has one",
tokenScopes: []string{"repo"},
acceptedScopes: []string{"repo", "admin:org"},
expected: true,
},
{
name: "empty token scopes - fails when scopes required",
tokenScopes: []string{},
acceptedScopes: []string{"repo"},
expected: false,
},
{
name: "user scope grants read:user",
tokenScopes: []string{"user"},
acceptedScopes: []string{"read:user"},
expected: true,
},
{
name: "user scope grants user:email",
tokenScopes: []string{"user"},
acceptedScopes: []string{"user:email"},
expected: true,
},
{
name: "write:packages grants read:packages",
tokenScopes: []string{"write:packages"},
acceptedScopes: []string{"read:packages"},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HasRequiredScopes(tt.tokenScopes, tt.acceptedScopes)
assert.Equal(t, tt.expected, result)
})
}
}