package scopes
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseScopeHeader(t *testing.T) {
tests := []struct {
name string
header string
expected []string
}{
{
name: "empty header",
header: "",
expected: []string{},
},
{
name: "single scope",
header: "repo",
expected: []string{"repo"},
},
{
name: "multiple scopes",
header: "repo, user, gist",
expected: []string{"repo", "user", "gist"},
},
{
name: "scopes with extra whitespace",
header: " repo , user , gist ",
expected: []string{"repo", "user", "gist"},
},
{
name: "scopes without spaces",
header: "repo,user,gist",
expected: []string{"repo", "user", "gist"},
},
{
name: "scopes with colons",
header: "read:org, write:org, admin:org",
expected: []string{"read:org", "write:org", "admin:org"},
},
{
name: "empty parts are filtered",
header: "repo,,gist",
expected: []string{"repo", "gist"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseScopeHeader(tt.header)
assert.Equal(t, tt.expected, result)
})
}
}
func TestFetcher_FetchTokenScopes(t *testing.T) {
tests := []struct {
name string
handler http.HandlerFunc
expectedScopes []string
expectError bool
errorContains string
}{
{
name: "successful fetch with multiple scopes",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-OAuth-Scopes", "repo, user, gist")
w.WriteHeader(http.StatusOK)
},
expectedScopes: []string{"repo", "user", "gist"},
expectError: false,
},
{
name: "successful fetch with single scope",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-OAuth-Scopes", "repo")
w.WriteHeader(http.StatusOK)
},
expectedScopes: []string{"repo"},
expectError: false,
},
{
name: "fine-grained PAT returns empty scopes",
handler: func(w http.ResponseWriter, _ *http.Request) {
// Fine-grained PATs don't return X-OAuth-Scopes
w.WriteHeader(http.StatusOK)
},
expectedScopes: []string{},
expectError: false,
},
{
name: "unauthorized token",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
},
expectError: true,
errorContains: "invalid or expired token",
},
{
name: "server error",
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
expectError: true,
errorContains: "unexpected status code: 500",
},
{
name: "verifies authorization header is set",
handler: func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader != "Bearer test-token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("X-OAuth-Scopes", "repo")
w.WriteHeader(http.StatusOK)
},
expectedScopes: []string{"repo"},
expectError: false,
},
{
name: "verifies request method is HEAD",
handler: func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("X-OAuth-Scopes", "repo")
w.WriteHeader(http.StatusOK)
},
expectedScopes: []string{"repo"},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(tt.handler)
defer server.Close()
fetcher := NewFetcher(FetcherOptions{
APIHost: server.URL,
})
scopes, err := fetcher.FetchTokenScopes(context.Background(), "test-token")
if tt.expectError {
require.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedScopes, scopes)
}
})
}
}
func TestFetcher_DefaultOptions(t *testing.T) {
fetcher := NewFetcher(FetcherOptions{})
// Verify default API host is set
assert.Equal(t, "https://api.github.com", fetcher.apiHost)
// Verify default HTTP client is set with timeout
assert.NotNil(t, fetcher.client)
assert.Equal(t, DefaultFetchTimeout, fetcher.client.Timeout)
}
func TestFetcher_CustomHTTPClient(t *testing.T) {
customClient := &http.Client{Timeout: 5 * time.Second}
fetcher := NewFetcher(FetcherOptions{
HTTPClient: customClient,
})
assert.Equal(t, customClient, fetcher.client)
}
func TestFetcher_CustomAPIHost(t *testing.T) {
fetcher := NewFetcher(FetcherOptions{
APIHost: "https://api.github.enterprise.com",
})
assert.Equal(t, "https://api.github.enterprise.com", fetcher.apiHost)
}
func TestFetcher_ContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(100 * time.Millisecond)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
fetcher := NewFetcher(FetcherOptions{
APIHost: server.URL,
})
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := fetcher.FetchTokenScopes(ctx, "test-token")
require.Error(t, err)
}