28b868dad4
* Apply lockdown mode to issues and pull requests * Add cache * Unlock in defer * Add muesli/cache2go * [WIP] Replace custom cache in lockdown.go with cache2go struct (#1425) * Initial plan * Replace custom cache with cache2go library - Added github.com/muesli/cache2go dependency - Replaced custom map-based cache with cache2go.CacheTable - Removed manual timer management (scheduleExpiry, ensureEntry methods) - Removed timer field from repoAccessCacheEntry struct - Updated GetRepoAccessInfo to use cache2go's Value() and Add() methods - Updated SetTTL to flush and re-add entries with new TTL - Used unique cache names per instance to avoid test interference - All existing tests pass with the new implementation Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Final verification complete Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Use muesli for cache * Make RepoAccessCache a singleton (#1426) * Initial plan * Implement RepoAccessCache as a singleton pattern Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Complete singleton implementation and verification Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Remove cacheIDCounter as requested Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JoannaaKL <67866556+JoannaaKL@users.noreply.github.com> * Update mutexes * . * Reuse cache * . * . * Fix logic after vibe coding * Update docs * . * Refactoring to make the code pretty * Hide lockdown logic behind shouldFilter function * . * Tests --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
113 lines
2.6 KiB
Go
113 lines
2.6 KiB
Go
package lockdown
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/github/github-mcp-server/internal/githubv4mock"
|
|
"github.com/shurcooL/githubv4"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testOwner = "octo-org"
|
|
testRepo = "octo-repo"
|
|
testUser = "octocat"
|
|
)
|
|
|
|
type repoAccessQuery struct {
|
|
Viewer struct {
|
|
Login githubv4.String
|
|
}
|
|
Repository struct {
|
|
IsPrivate githubv4.Boolean
|
|
Collaborators struct {
|
|
Edges []struct {
|
|
Permission githubv4.String
|
|
Node struct {
|
|
Login githubv4.String
|
|
}
|
|
}
|
|
} `graphql:"collaborators(query: $username, first: 1)"`
|
|
} `graphql:"repository(owner: $owner, name: $name)"`
|
|
}
|
|
|
|
type countingTransport struct {
|
|
mu sync.Mutex
|
|
next http.RoundTripper
|
|
calls int
|
|
}
|
|
|
|
func (c *countingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
c.mu.Lock()
|
|
c.calls++
|
|
c.mu.Unlock()
|
|
return c.next.RoundTrip(req)
|
|
}
|
|
|
|
func (c *countingTransport) CallCount() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.calls
|
|
}
|
|
|
|
func newMockRepoAccessCache(t *testing.T, ttl time.Duration) (*RepoAccessCache, *countingTransport) {
|
|
t.Helper()
|
|
|
|
var query repoAccessQuery
|
|
|
|
variables := map[string]any{
|
|
"owner": githubv4.String(testOwner),
|
|
"name": githubv4.String(testRepo),
|
|
"username": githubv4.String(testUser),
|
|
}
|
|
|
|
response := githubv4mock.DataResponse(map[string]any{
|
|
"viewer": map[string]any{
|
|
"login": testUser,
|
|
},
|
|
"repository": map[string]any{
|
|
"isPrivate": false,
|
|
"collaborators": map[string]any{
|
|
"edges": []any{
|
|
map[string]any{
|
|
"permission": "WRITE",
|
|
"node": map[string]any{
|
|
"login": testUser,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
httpClient := githubv4mock.NewMockedHTTPClient(githubv4mock.NewQueryMatcher(query, variables, response))
|
|
counting := &countingTransport{next: httpClient.Transport}
|
|
httpClient.Transport = counting
|
|
|
|
gqlClient := githubv4.NewClient(httpClient)
|
|
|
|
return GetInstance(gqlClient, WithTTL(ttl)), counting
|
|
}
|
|
|
|
func TestRepoAccessCacheEvictsAfterTTL(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
cache, transport := newMockRepoAccessCache(t, 5*time.Millisecond)
|
|
info, err := cache.getRepoAccessInfo(ctx, testUser, testOwner, testRepo)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testUser, info.ViewerLogin)
|
|
require.True(t, info.HasPushAccess)
|
|
require.EqualValues(t, 1, transport.CallCount())
|
|
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
info, err = cache.getRepoAccessInfo(ctx, testUser, testOwner, testRepo)
|
|
require.NoError(t, err)
|
|
require.Equal(t, testUser, info.ViewerLogin)
|
|
require.True(t, info.HasPushAccess)
|
|
require.EqualValues(t, 2, transport.CallCount())
|
|
}
|