chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
ghcontext "github.com/github/github-mcp-server/pkg/context"
|
||||
"github.com/github/github-mcp-server/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockScopeFetcher is a mock implementation of scopes.FetcherInterface
|
||||
type mockScopeFetcher struct {
|
||||
scopes []string
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockScopeFetcher) FetchTokenScopes(_ context.Context, _ string) ([]string, error) {
|
||||
return m.scopes, m.err
|
||||
}
|
||||
|
||||
func TestWithPATScopes(t *testing.T) {
|
||||
logger := slog.Default()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenInfo *ghcontext.TokenInfo
|
||||
fetcherScopes []string
|
||||
fetcherErr error
|
||||
expectScopesFetched bool
|
||||
expectedScopes []string
|
||||
expectNextHandlerCalled bool
|
||||
}{
|
||||
{
|
||||
name: "no token info in context calls next handler",
|
||||
tokenInfo: nil,
|
||||
expectScopesFetched: false,
|
||||
expectedScopes: nil,
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
{
|
||||
name: "non-PAT token type skips scope fetching",
|
||||
tokenInfo: &ghcontext.TokenInfo{
|
||||
Token: "gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
TokenType: utils.TokenTypeOAuthAccessToken,
|
||||
},
|
||||
expectScopesFetched: false,
|
||||
expectedScopes: nil,
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
{
|
||||
name: "fine-grained PAT skips scope fetching",
|
||||
tokenInfo: &ghcontext.TokenInfo{
|
||||
Token: "github_pat_xxxxxxxxxxxxxxxxxxxxxxx",
|
||||
TokenType: utils.TokenTypeFineGrainedPersonalAccessToken,
|
||||
},
|
||||
expectScopesFetched: false,
|
||||
expectedScopes: nil,
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
{
|
||||
name: "classic PAT fetches and stores scopes",
|
||||
tokenInfo: &ghcontext.TokenInfo{
|
||||
Token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
TokenType: utils.TokenTypePersonalAccessToken,
|
||||
},
|
||||
fetcherScopes: []string{"repo", "user", "read:org"},
|
||||
expectScopesFetched: true,
|
||||
expectedScopes: []string{"repo", "user", "read:org"},
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
{
|
||||
name: "classic PAT with empty scopes",
|
||||
tokenInfo: &ghcontext.TokenInfo{
|
||||
Token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
TokenType: utils.TokenTypePersonalAccessToken,
|
||||
},
|
||||
fetcherScopes: []string{},
|
||||
expectScopesFetched: true,
|
||||
expectedScopes: []string{},
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
{
|
||||
name: "fetcher error calls next handler without scopes",
|
||||
tokenInfo: &ghcontext.TokenInfo{
|
||||
Token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
TokenType: utils.TokenTypePersonalAccessToken,
|
||||
},
|
||||
fetcherErr: errors.New("network error"),
|
||||
expectScopesFetched: false,
|
||||
expectedScopes: nil,
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
{
|
||||
name: "old-style PAT (40 hex chars) fetches scopes",
|
||||
tokenInfo: &ghcontext.TokenInfo{
|
||||
Token: "0123456789abcdef0123456789abcdef01234567",
|
||||
TokenType: utils.TokenTypePersonalAccessToken,
|
||||
},
|
||||
fetcherScopes: []string{"repo"},
|
||||
expectScopesFetched: true,
|
||||
expectedScopes: []string{"repo"},
|
||||
expectNextHandlerCalled: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedScopes []string
|
||||
var scopesFound bool
|
||||
var nextHandlerCalled bool
|
||||
|
||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
nextHandlerCalled = true
|
||||
capturedScopes, scopesFound = ghcontext.GetTokenScopes(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
fetcher := &mockScopeFetcher{
|
||||
scopes: tt.fetcherScopes,
|
||||
err: tt.fetcherErr,
|
||||
}
|
||||
|
||||
middleware := WithPATScopes(logger, fetcher)
|
||||
handler := middleware(nextHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
// Set up context with token info if provided
|
||||
if tt.tokenInfo != nil {
|
||||
ctx := ghcontext.WithTokenInfo(req.Context(), tt.tokenInfo)
|
||||
req = req.WithContext(ctx)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, tt.expectNextHandlerCalled, nextHandlerCalled, "next handler called mismatch")
|
||||
|
||||
if tt.expectNextHandlerCalled {
|
||||
assert.Equal(t, tt.expectScopesFetched, scopesFound, "scopes found mismatch")
|
||||
assert.Equal(t, tt.expectedScopes, capturedScopes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithPATScopes_PreservesExistingTokenInfo(t *testing.T) {
|
||||
logger := slog.Default()
|
||||
|
||||
var capturedTokenInfo *ghcontext.TokenInfo
|
||||
var capturedScopes []string
|
||||
var scopesFound bool
|
||||
|
||||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedTokenInfo, _ = ghcontext.GetTokenInfo(r.Context())
|
||||
capturedScopes, scopesFound = ghcontext.GetTokenScopes(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
fetcher := &mockScopeFetcher{
|
||||
scopes: []string{"repo", "user"},
|
||||
}
|
||||
|
||||
originalTokenInfo := &ghcontext.TokenInfo{
|
||||
Token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
TokenType: utils.TokenTypePersonalAccessToken,
|
||||
}
|
||||
|
||||
middleware := WithPATScopes(logger, fetcher)
|
||||
handler := middleware(nextHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
ctx := ghcontext.WithTokenInfo(req.Context(), originalTokenInfo)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
require.NotNil(t, capturedTokenInfo)
|
||||
assert.Equal(t, originalTokenInfo.Token, capturedTokenInfo.Token)
|
||||
assert.Equal(t, originalTokenInfo.TokenType, capturedTokenInfo.TokenType)
|
||||
assert.True(t, scopesFound)
|
||||
assert.Equal(t, []string{"repo", "user"}, capturedScopes)
|
||||
}
|
||||
Reference in New Issue
Block a user