278 lines
9.7 KiB
Go
278 lines
9.7 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type stubInstallationTokenMinter struct {
|
|
calls atomic.Int64
|
|
mint func(context.Context, int64, InstallationPermissions, []string) (InstallationToken, error)
|
|
}
|
|
|
|
func (s *stubInstallationTokenMinter) MintInstallationToken(
|
|
ctx context.Context,
|
|
installationID int64,
|
|
permissions InstallationPermissions,
|
|
repositories []string,
|
|
) (InstallationToken, error) {
|
|
s.calls.Add(1)
|
|
return s.mint(ctx, installationID, permissions, repositories)
|
|
}
|
|
|
|
func TestInstallationTokenCache_CoalescesAndRefreshes(t *testing.T) {
|
|
now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
|
|
release := make(chan struct{})
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, installationID int64, permissions InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
<-release
|
|
return InstallationToken{Token: "first", ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
permissions := InstallationPermissions{"contents": PermissionWrite}
|
|
|
|
const callers = 20
|
|
var wg sync.WaitGroup
|
|
var ready sync.WaitGroup
|
|
wg.Add(callers)
|
|
ready.Add(callers)
|
|
start := make(chan struct{})
|
|
errs := make(chan error, callers)
|
|
for range callers {
|
|
go func() {
|
|
defer wg.Done()
|
|
ready.Done()
|
|
<-start
|
|
token, err := cache.Get(context.Background(), 42, permissions, nil)
|
|
if err == nil && token.Token != "first" {
|
|
err = errors.New("unexpected token")
|
|
}
|
|
errs <- err
|
|
}()
|
|
}
|
|
ready.Wait()
|
|
close(start)
|
|
close(release)
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
}
|
|
if got := minter.calls.Load(); got != 1 {
|
|
t.Fatalf("mint calls = %d, want 1", got)
|
|
}
|
|
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
return InstallationToken{Token: "second", ExpiresAt: now.Add(2 * time.Hour)}, nil
|
|
}
|
|
now = now.Add(56 * time.Minute)
|
|
token, err := cache.Get(context.Background(), 42, permissions, nil)
|
|
if err != nil {
|
|
t.Fatalf("refresh Get: %v", err)
|
|
}
|
|
if token.Token != "second" || minter.calls.Load() != 2 {
|
|
t.Fatalf("refresh token = %q, calls = %d", token.Token, minter.calls.Load())
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_RefreshFailureUsesOnlyUnexpiredToken(t *testing.T) {
|
|
now := time.Now()
|
|
mintErr := errors.New("mint failed")
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
if minter.calls.Load() == 1 {
|
|
return InstallationToken{Token: "first", ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
return InstallationToken{}, mintErr
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
first, err := cache.Get(context.Background(), 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("first Get: %v", err)
|
|
}
|
|
|
|
now = now.Add(56 * time.Minute)
|
|
fallback, err := cache.Get(context.Background(), 1, nil, nil)
|
|
if err != nil || fallback.Token != first.Token {
|
|
t.Fatalf("valid fallback = %+v, %v", fallback, err)
|
|
}
|
|
|
|
now = now.Add(5 * time.Minute)
|
|
if _, err := cache.Get(context.Background(), 1, nil, nil); !errors.Is(err, mintErr) {
|
|
t.Fatalf("expired fallback error = %v, want mint error", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_InvalidateRejectsInflightMint(t *testing.T) {
|
|
now := time.Now()
|
|
started := make(chan struct{})
|
|
release := make(chan struct{})
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
close(started)
|
|
<-release
|
|
return InstallationToken{Token: "stale", ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
result := make(chan error, 1)
|
|
go func() {
|
|
_, err := cache.Get(context.Background(), 1, nil, nil)
|
|
result <- err
|
|
}()
|
|
<-started
|
|
cache.Invalidate(1)
|
|
close(release)
|
|
if err := <-result; !errors.Is(err, ErrInstallationTokenInvalidated) {
|
|
t.Fatalf("in-flight Get error = %v, want invalidated", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_PermissionScopeIsPartOfKey(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
return InstallationToken{Token: "token", ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
_, _ = cache.Get(context.Background(), 1, InstallationPermissions{"contents": PermissionRead}, nil)
|
|
_, _ = cache.Get(context.Background(), 1, InstallationPermissions{"contents": PermissionWrite}, nil)
|
|
_, _ = cache.Get(context.Background(), 2, InstallationPermissions{"contents": PermissionRead}, nil)
|
|
if got := minter.calls.Load(); got != 3 {
|
|
t.Fatalf("mint calls = %d, want 3 distinct cache keys", got)
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_RepositoryScopeIsPartOfKey(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, repositories []string) (InstallationToken, error) {
|
|
return InstallationToken{Token: strings.Join(repositories, ","), ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
first, err := cache.Get(context.Background(), 1, nil, []string{"frontend"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := cache.Get(context.Background(), 1, nil, []string{"backend"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := cache.Get(context.Background(), 1, nil, []string{"frontend"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Token != "frontend" || second.Token != "backend" || again.Token != first.Token || minter.calls.Load() != 2 {
|
|
t.Fatalf("tokens = %q/%q/%q, calls = %d", first.Token, second.Token, again.Token, minter.calls.Load())
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_NeverReturnsExpiredToken(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
return InstallationToken{Token: "expired", ExpiresAt: now}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
if _, err := cache.Get(context.Background(), 1, nil, nil); !errors.Is(err, ErrInstallationTokenExpired) {
|
|
t.Fatalf("Get error = %v, want ErrInstallationTokenExpired", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_InvalidateDropsCachedToken(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
call := minter.calls.Load()
|
|
return InstallationToken{Token: string(rune('a' + call)), ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
first, err := cache.Get(context.Background(), 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("first Get: %v", err)
|
|
}
|
|
cache.Invalidate(1)
|
|
second, err := cache.Get(context.Background(), 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("second Get: %v", err)
|
|
}
|
|
if first.Token == second.Token || minter.calls.Load() != 2 {
|
|
t.Fatalf("tokens = %q/%q, calls = %d", first.Token, second.Token, minter.calls.Load())
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCache_ReturnedPermissionsCannotMutateCache(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(_ context.Context, _ int64, _ InstallationPermissions, _ []string) (InstallationToken, error) {
|
|
return InstallationToken{
|
|
Token: "token",
|
|
ExpiresAt: now.Add(time.Hour),
|
|
Permissions: InstallationPermissions{"contents": PermissionRead},
|
|
}, nil
|
|
}
|
|
cache := NewInstallationTokenCache(minter)
|
|
cache.now = func() time.Time { return now }
|
|
first, err := cache.Get(context.Background(), 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("first Get: %v", err)
|
|
}
|
|
first.Permissions["contents"] = PermissionWrite
|
|
second, err := cache.Get(context.Background(), 1, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("second Get: %v", err)
|
|
}
|
|
if second.Permissions["contents"] != PermissionRead {
|
|
t.Fatalf("cached permissions mutated to %q", second.Permissions["contents"])
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCacheCarriesRegistrationIdentity(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(context.Context, int64, InstallationPermissions, []string) (InstallationToken, error) {
|
|
return InstallationToken{Token: "token", ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewAppInstallationTokenCache("registration-a", 9, minter)
|
|
cache.now = func() time.Time { return now }
|
|
token, err := cache.Get(context.Background(), 42, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if token.Principal.AppRegistrationID != "registration-a" ||
|
|
token.Principal.AppCredentialGeneration != 9 {
|
|
t.Fatalf("token principal = %+v", token.Principal)
|
|
}
|
|
}
|
|
|
|
func TestInstallationTokenCacheSeparatesWorkspacesReusingRegistration(t *testing.T) {
|
|
now := time.Now()
|
|
minter := &stubInstallationTokenMinter{}
|
|
minter.mint = func(context.Context, int64, InstallationPermissions, []string) (InstallationToken, error) {
|
|
return InstallationToken{Token: "token", ExpiresAt: now.Add(time.Hour)}, nil
|
|
}
|
|
cache := NewAppInstallationTokenCache("registration-a", 9, minter)
|
|
cache.now = func() time.Time { return now }
|
|
for _, workspaceID := range []string{"workspace-a", "workspace-b"} {
|
|
if _, err := cache.GetForWorkspace(context.Background(), workspaceID, 42, nil, []string{"repo"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if got := minter.calls.Load(); got != 2 {
|
|
t.Fatalf("mint calls = %d, want one isolated token per workspace", got)
|
|
}
|
|
}
|