Files
kandev/apps/backend/internal/github/service_fork_test.go
T

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)
}