418 lines
14 KiB
Go
418 lines
14 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
type forkResolverFakeClient struct {
|
|
user string
|
|
repos map[string]*GitHubRepository
|
|
forks []*GitHubRepository
|
|
getErrors map[string]error
|
|
getCalls map[string]int
|
|
readyAfter int
|
|
created []string
|
|
createRepo *GitHubRepository
|
|
}
|
|
|
|
type contributionDestinationVerificationClient struct {
|
|
Client
|
|
repository *GitHubRepository
|
|
}
|
|
|
|
func (c *contributionDestinationVerificationClient) GetRepository(context.Context, string, string) (*GitHubRepository, error) {
|
|
return copyGitHubRepository(c.repository), nil
|
|
}
|
|
|
|
func (f *forkResolverFakeClient) GetAuthenticatedUser(context.Context) (string, error) {
|
|
return f.user, nil
|
|
}
|
|
|
|
func (f *forkResolverFakeClient) GetRepository(_ context.Context, owner, repo string) (*GitHubRepository, error) {
|
|
key := owner + "/" + repo
|
|
if f.getCalls == nil {
|
|
f.getCalls = make(map[string]int)
|
|
}
|
|
f.getCalls[key]++
|
|
if err := f.getErrors[key]; err != nil {
|
|
return nil, err
|
|
}
|
|
repository, ok := f.repos[key]
|
|
if !ok {
|
|
return nil, &GitHubAPIError{StatusCode: 404, Endpoint: "/repos/" + key}
|
|
}
|
|
if f.readyAfter > 0 && key != "kdlbs/kandev" && f.getCalls[key] >= f.readyAfter {
|
|
repository.PushAccess = true
|
|
repository.AdminAccess = true
|
|
}
|
|
return repository, nil
|
|
}
|
|
|
|
func (f *forkResolverFakeClient) CreateFork(_ context.Context, owner, repo string) (*GitHubRepository, error) {
|
|
f.created = append(f.created, owner+"/"+repo)
|
|
if f.createRepo == nil {
|
|
return nil, errors.New("fake fork creation failed")
|
|
}
|
|
f.repos[f.createRepo.FullName] = f.createRepo
|
|
return f.createRepo, nil
|
|
}
|
|
|
|
func (f *forkResolverFakeClient) ListRepositoryForks(context.Context, string, string) ([]*GitHubRepository, error) {
|
|
result := make([]*GitHubRepository, 0, len(f.forks))
|
|
for _, fork := range f.forks {
|
|
result = append(result, copyGitHubRepository(fork))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func TestResolveContributionForkUsesDirectTargetWrite(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", true)
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical},
|
|
}
|
|
|
|
result, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveContributionFork: %v", err)
|
|
}
|
|
if result.Status != ContributionForkStatusDirectWrite {
|
|
t.Fatalf("status = %q, want %q", result.Status, ContributionForkStatusDirectWrite)
|
|
}
|
|
if result.Destination != nil {
|
|
t.Fatalf("direct target returned destination: %#v", result.Destination)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks = %v, want none", client.created)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkAcceptsOnlyExactWritableFork(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("alice/kandev", "200", true)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical, "alice/kandev": fork},
|
|
}
|
|
|
|
result, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveContributionFork: %v", err)
|
|
}
|
|
if result.Status != ContributionForkStatusReady {
|
|
t.Fatalf("status = %q, want %q", result.Status, ContributionForkStatusReady)
|
|
}
|
|
if result.Destination == nil {
|
|
t.Fatal("exact fork did not return a destination")
|
|
}
|
|
if got := result.Destination.TargetRepository.Path; got != "alice/kandev" {
|
|
t.Fatalf("destination target = %q, want alice/kandev", got)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks = %v, want none", client.created)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkReusesRenamedForkFromForkNetwork(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("alice/kandev-renamed", "200", true)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical, "alice/kandev-renamed": fork},
|
|
getErrors: map[string]error{
|
|
"alice/kandev": &GitHubAPIError{StatusCode: 404, Endpoint: "/repos/alice/kandev"},
|
|
},
|
|
forks: []*GitHubRepository{fork},
|
|
}
|
|
|
|
result, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveContributionFork: %v", err)
|
|
}
|
|
if result.Destination == nil || result.Destination.TargetRepository.Path != "alice/kandev-renamed" {
|
|
t.Fatalf("destination = %#v, want the renamed fork", result.Destination)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks = %v, want none", client.created)
|
|
}
|
|
second, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("second resolveContributionFork: %v", err)
|
|
}
|
|
if second.Destination == nil || second.Destination.TargetRepository.Path != "alice/kandev-renamed" {
|
|
t.Fatalf("second destination = %#v, want the same renamed fork", second.Destination)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks after second resolution = %v, want none", client.created)
|
|
}
|
|
}
|
|
|
|
func TestBindContributionDestinationCredentialCapturesConnectionGeneration(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("alice/kandev", "200", true)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
destination, err := makeContributionDestination(canonical, fork)
|
|
if err != nil {
|
|
t.Fatalf("makeContributionDestination: %v", err)
|
|
}
|
|
resolved := &resolvedServiceClient{
|
|
credential: &ResolvedCredential{
|
|
Principal: AuthPrincipal{Source: ConnectionSourcePAT, Login: "alice"},
|
|
CredentialGeneration: 9,
|
|
},
|
|
}
|
|
|
|
if err := bindContributionDestinationCredential(destination, resolved); err != nil {
|
|
t.Fatalf("bindContributionDestinationCredential: %v", err)
|
|
}
|
|
if destination.CredentialBinding == nil || destination.CredentialBinding.Source != string(ConnectionSourcePAT) ||
|
|
destination.CredentialBinding.Login != "alice" || destination.CredentialBinding.CredentialGeneration != 9 {
|
|
t.Fatalf("credential binding = %#v", destination.CredentialBinding)
|
|
}
|
|
}
|
|
|
|
func TestVerifyContributionDestinationForWorkspaceRejectsRecreatedTarget(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("automation/kandev", "200", true)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
client := &contributionDestinationVerificationClient{Client: NewMockClient(), repository: fork}
|
|
connections := testConnectionReader{workspaces: map[string]*WorkspaceConnection{
|
|
"workspace-1": {
|
|
WorkspaceID: "workspace-1", Source: ConnectionSourcePAT, Login: "automation",
|
|
Status: ConnectionStatusActive, CredentialGeneration: 1,
|
|
},
|
|
}}
|
|
resolver := NewCredentialResolver(connections, fakeAuthSecrets{
|
|
WorkspacePATSecretKey("workspace-1"): "workspace-token",
|
|
})
|
|
resolver.SetAutomationProvider(testAutomationCredentialProvider{client: client})
|
|
service := &Service{resolver: resolver}
|
|
|
|
verify := func() error {
|
|
return service.VerifyContributionDestinationForWorkspace(
|
|
context.Background(), "workspace-1", "kdlbs", "kandev", "100", "automation", "kandev", "200",
|
|
)
|
|
}
|
|
if err := verify(); err != nil {
|
|
t.Fatalf("VerifyContributionDestinationForWorkspace() error = %v", err)
|
|
}
|
|
|
|
client.repository.ID = 201
|
|
if err := verify(); err == nil {
|
|
t.Fatal("recreated target with a different provider ID was accepted")
|
|
}
|
|
client.repository.ID = 200
|
|
client.repository.ParentID = 101
|
|
if err := verify(); err == nil {
|
|
t.Fatal("target with a different parent provider ID was accepted")
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkRejectsExistingNonWritableFork(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("alice/kandev", "200", false)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical, "alice/kandev": fork},
|
|
}
|
|
|
|
_, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if !errors.Is(err, ErrContributionForkNotWritable) {
|
|
t.Fatalf("error = %v, want ErrContributionForkNotWritable", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkRejectsWrongParent(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
wrongParent := testGitHubRepository("alice/kandev", "200", true)
|
|
wrongParent.Fork = true
|
|
wrongParent.ParentID = 999
|
|
wrongParent.ParentFullName = "other/project"
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical, "alice/kandev": wrongParent},
|
|
}
|
|
|
|
_, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if !errors.Is(err, ErrContributionForkConflict) {
|
|
t.Fatalf("error = %v, want ErrContributionForkConflict", err)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks = %v, want none", client.created)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkBlocksAppWithoutDirectWrite(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
client := &forkResolverFakeClient{
|
|
user: "kdlbs-app[bot]",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical},
|
|
}
|
|
|
|
_, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalApp, Login: "kdlbs-app[bot]"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if !errors.Is(err, ErrContributionForkAppUnsupported) {
|
|
t.Fatalf("error = %v, want ErrContributionForkAppUnsupported", err)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks = %v, want none", client.created)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkReportsCreatableWithoutCreating(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical},
|
|
getErrors: map[string]error{
|
|
"alice/kandev": &GitHubAPIError{StatusCode: 404, Endpoint: "/repos/alice/kandev"},
|
|
},
|
|
}
|
|
|
|
result, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Source: ConnectionSourceGHCLI, Login: "alice"},
|
|
"kdlbs", "kandev", false,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveContributionFork: %v", err)
|
|
}
|
|
if result.Status != ContributionForkStatusCreatable {
|
|
t.Fatalf("status = %q, want %q", result.Status, ContributionForkStatusCreatable)
|
|
}
|
|
if result.Destination != nil {
|
|
t.Fatalf("creatable probe returned destination: %#v", result.Destination)
|
|
}
|
|
if len(client.created) != 0 {
|
|
t.Fatalf("created forks = %v, want none", client.created)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkCreatesAndVerifiesMissingHumanFork(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("alice/kandev", "200", true)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical},
|
|
createRepo: fork,
|
|
}
|
|
|
|
result, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Source: ConnectionSourcePAT, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveContributionFork: %v", err)
|
|
}
|
|
if result.Status != ContributionForkStatusReady || result.Destination == nil {
|
|
t.Fatalf("result = %#v, want ready destination", result)
|
|
}
|
|
if len(client.created) != 1 || client.created[0] != "kdlbs/kandev" {
|
|
t.Fatalf("created forks = %v, want [kdlbs/kandev]", client.created)
|
|
}
|
|
}
|
|
|
|
func TestResolveContributionForkUsesCreatedRepositoryIdentityAndWaitsForReadiness(t *testing.T) {
|
|
canonical := testGitHubRepository("kdlbs/kandev", "100", false)
|
|
fork := testGitHubRepository("alice/kandev-renamed", "200", false)
|
|
fork.Fork = true
|
|
fork.ParentID = canonical.ID
|
|
fork.ParentFullName = canonical.FullName
|
|
client := &forkResolverFakeClient{
|
|
user: "alice",
|
|
repos: map[string]*GitHubRepository{"kdlbs/kandev": canonical},
|
|
createRepo: fork,
|
|
readyAfter: 11,
|
|
}
|
|
|
|
result, err := resolveContributionFork(
|
|
context.Background(), client, AuthPrincipal{Kind: AuthPrincipalHuman, Source: ConnectionSourcePAT, Login: "alice"},
|
|
"kdlbs", "kandev", true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveContributionFork: %v", err)
|
|
}
|
|
if result.Destination == nil || result.Destination.TargetRepository.Path != "alice/kandev-renamed" {
|
|
t.Fatalf("destination = %#v, want created repository identity", result.Destination)
|
|
}
|
|
if client.getCalls["alice/kandev-renamed"] < client.readyAfter {
|
|
t.Fatalf("fork readiness checks = %d, want at least %d", client.getCalls["alice/kandev-renamed"], client.readyAfter)
|
|
}
|
|
}
|
|
|
|
func TestIsContributionRepositoryNotFoundRequiresProviderError(t *testing.T) {
|
|
for _, err := range []error{
|
|
errors.New("endpoint /repos/alice/404-user returned 500"),
|
|
errors.New("repository not found in a wrapped provider failure"),
|
|
} {
|
|
if isContributionRepositoryNotFound(err) {
|
|
t.Fatalf("isContributionRepositoryNotFound(%v) = true, want false", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testGitHubRepository(fullName, id string, push bool) *GitHubRepository {
|
|
var numericID int64
|
|
if _, err := fmt.Sscan(id, &numericID); err != nil {
|
|
panic(err)
|
|
}
|
|
parts := splitRepositoryFullName(fullName)
|
|
return &GitHubRepository{
|
|
ID: numericID,
|
|
FullName: fullName,
|
|
Owner: parts[0],
|
|
Name: parts[1],
|
|
CloneURL: "https://github.com/" + fullName + ".git",
|
|
Fork: false,
|
|
PushAccess: push,
|
|
AdminAccess: push,
|
|
}
|
|
}
|
|
|
|
func splitRepositoryFullName(fullName string) []string {
|
|
for index, character := range fullName {
|
|
if character == '/' {
|
|
return []string{fullName[:index], fullName[index+1:]}
|
|
}
|
|
}
|
|
panic("invalid repository full name: " + fullName)
|
|
}
|