diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4bfb399..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: amitsaha diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7acf075 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + +jobs: + linux_tests: + runs-on: ubuntu-latest + strategy: + matrix: + go: [ '1.16', '1.17' ] + name: Go ${{ matrix.go }} tests - Linux + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - run: go test + + macos_tests: + runs-on: macOS-latest + strategy: + matrix: + go: [ '1.16', '1.17' ] + name: Go ${{ matrix.go }} tests - MacOS + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - run: go test + + windows_tests: + runs-on: windows-latest + strategy: + matrix: + go: [ '1.16', '1.17' ] + name: Go ${{ matrix.go }} tests - Windows + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - run: go test + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 047a003..6b6dc20 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'go', 'python' ] + language: [ 'go'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index ab11a2c..8c020aa 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -21,7 +21,7 @@ jobs: name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.15 + go-version: 1.17 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0b03a38..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: go -os: - - linux - - osx -go: - - 1.15.x - -install: true - -script: - - go get -u golang.org/x/lint/golint - - cd $GOPATH/src/github.com/amitsaha/gitbackup/ - - bash ./check_test.bash - - bash ./build-binaries.bash - -deploy: - provider: releases - api_key: - secure: $GITBACKUP_RELEASE - file_glob: true - file: artifacts/gitbackup-* - skip_cleanup: true - on: - tags: true - draft: true diff --git a/README.md b/README.md index b4ffa64..e874ae3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,28 @@ # gitbackup - Backup your GitHub, GitLab, and Bitbucket repositories Code Quality [![Go Report Card](https://goreportcard.com/badge/github.com/amitsaha/gitbackup)](https://goreportcard.com/report/github.com/amitsaha/gitbackup) -Linux/Mac OS X [![Build Status](https://travis-ci.org/amitsaha/gitbackup.svg?branch=master)](https://travis-ci.org/amitsaha/gitbackup) Windows [![Build status](https://ci.appveyor.com/api/projects/status/fwki40x1havyian2/branch/master?svg=true)](https://ci.appveyor.com/project/amitsaha/gitbackup/branch/master) +[![.github/workflows/ci.yml](https://github.com/amitsaha/gitbackup/actions/workflows/ci.yml/badge.svg)](https://github.com/amitsaha/gitbackup/actions/workflows/ci.yml) + +- [gitbackup - Backup your GitHub, GitLab, and Bitbucket repositories](#gitbackup---backup-your-github-gitlab-and-bitbucket-repositories) + - [Introduction](#introduction) + - [Installing `gitbackup`](#installing-gitbackup) + - [Using `gitbackup`](#using-gitbackup) + - [GitHub Specific oAuth App Flow](#github-specific-oauth-app-flow) + - [OAuth Scopes/Permissions required](#oauth-scopespermissions-required) + - [Bitbucket](#bitbucket) + - [GitHub](#github) + - [GitLab](#gitlab) + - [Security and credentials](#security-and-credentials) + - [Examples](#examples) + - [Backing up your GitHub repositories](#backing-up-your-github-repositories) + - [Backing up your GitLab repositories](#backing-up-your-gitlab-repositories) + - [GitHub Enterprise or custom GitLab installation](#github-enterprise-or-custom-gitlab-installation) + - [Backing up your Bitbucket repositories](#backing-up-your-bitbucket-repositories) + - [Specifying a backup location](#specifying-a-backup-location) + - [Cloning bare repositories](#cloning-bare-repositories) + - [GitHub Migrations](#github-migrations) + - [Building](#building) + +## Introduction ``gitbackup`` is a tool to backup your git repositories from GitHub (including GitHub enterprise), GitLab (including custom GitLab installations), or Bitbucket. diff --git a/VERSION b/VERSION deleted file mode 100644 index e961155..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.09 diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index beb9d85..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: "{build}" - -# Build host - -environment: - matrix: - - environment: - GOVERSION: 1.15 - -# Build - -install: - # Install the specific Go version. - - rmdir c:\go /s /q - - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi - - msiexec /i go%GOVERSION%.windows-amd64.msi /q - -build: off - -test_script: - - go build -o bin\gitbackup.exe - - go test -v diff --git a/build-binaries.bash b/build-binaries.bash deleted file mode 100644 index 0f24577..0000000 --- a/build-binaries.bash +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -VERSION=$(git describe --abbrev=0 --tags) -DISTDIR="artifacts/" -export GO111MODULE=on - -for pair in linux/386 linux/amd64 linux/arm linux/arm64 darwin/amd64 dragonfly/amd64 freebsd/amd64 netbsd/amd64 openbsd/amd64 windows/amd64; do - GOOS=`echo $pair | cut -d'/' -f1` - GOARCH=`echo $pair | cut -d'/' -f2` - OBJECT_FILE="gitbackup-$VERSION-$GOOS-$GOARCH" - GOOS=$GOOS GOARCH=$GOARCH go build -o "$DISTDIR/$OBJECT_FILE" -done \ No newline at end of file diff --git a/build-binaries.py b/build-binaries.py deleted file mode 100644 index 57d0827..0000000 --- a/build-binaries.py +++ /dev/null @@ -1,26 +0,0 @@ -import subprocess -import os - -VERSION=subprocess.check_output(["git", "describe", "--abbrev=0", "--tags"]).decode("utf-8").rstrip("\n") -DISTDIR="./artifacts" - -distpairs = [ - "linux/386", - "linux/amd64", - "linux/arm", - "linux/arm64", - "darwin/amd64", - "dragonfly/amd64", - "freebsd/amd64", - "netbsd/amd64", - "openbsd/amd64", - "windows/amd64" -] - -for distpair in distpairs: - GOOS = distpair.split("/")[0] - GOARCH = distpair.split("/")[1] - OBJECT_FILE="gitbackup-{0}-{1}-{2}".format(VERSION, GOOS, GOARCH) - subprocess.check_output( - ["go", "build", "-o", "{0}/{1}".format(DISTDIR, OBJECT_FILE)], - env = {"GOOS": GOOS, "GOARCH": GOARCH, "GOPATH": os.environ.get("GOPATH"), "GOCACHE": "on", "GOROOT": os.environ.get("GOROOT")}) \ No newline at end of file diff --git a/check_test.bash b/check_test.bash deleted file mode 100644 index 1f88ae5..0000000 --- a/check_test.bash +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -e - -export GO111MODULE=on -go env -golint -set_exit_status -go build -go test \ No newline at end of file diff --git a/main.go b/main.go index b49116d..c0f0162 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "net/url" "sync" + "time" "github.com/google/go-github/v34/github" ) @@ -122,15 +123,19 @@ func main() { } if *githubWaitForMigrationComplete { - downloadGithubUserMigrationData(client, *backupDir, m.ID) + migrationStatePollingDuration := 60 * time.Second + err = downloadGithubUserMigrationData(context.Background(), client, *backupDir, m.ID, migrationStatePollingDuration) + if err != nil { + log.Fatalf("Error querying/downloading migration: %v", err) + } } - orgs, err := getUserOwnedOrgs(client) + orgs, err := getGithubUserOwnedOrgs(context.Background(), client) if err != nil { log.Fatal("Error getting user organizations", err) } for _, o := range orgs { - orgRepos, err := getGithubOrgRepositories(client, o) + orgRepos, err := getGithubOrgRepositories(context.Background(), client, o) if err != nil { log.Fatal("Error getting org repos", err) } @@ -144,7 +149,8 @@ func main() { log.Fatalf("Error creating migration: %v", err) } if *githubWaitForMigrationComplete { - downloadGithubOrgMigrationData(client, *o.Login, *backupDir, oMigration.ID) + migrationStatePollingDuration := 60 * time.Second + downloadGithubOrgMigrationData(context.Background(), client, *o.Login, *backupDir, oMigration.ID, migrationStatePollingDuration) } } diff --git a/release.bash b/release.bash deleted file mode 100755 index 7507e23..0000000 --- a/release.bash +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# Stolen from https://github.com/oklog/oklog/blob/master/release.fish -set -eu - -VERSION=$1 -git tag --annotate $VERSION -m "Release $VERSION" -git push origin $VERSION \ No newline at end of file diff --git a/user_data.go b/user_data.go index 46034f5..f72251f 100644 --- a/user_data.go +++ b/user_data.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -15,6 +16,27 @@ import ( "github.com/google/go-github/v34/github" ) +// Using vars insted of const, since we cannot take & of a const +// For now, these are all specific to github +var ( + migrationStatePending = "pending" + //migrationStateExporting = "exporting" + migrationStateExported = "exported" + migrationStateFailed = "failed" + + orgRoleMember = "member" + orgRoleMaintainer = "maintainer" + orgRoleAdmin = "admin" +) + +func getLocalMigrationFilepath(backupDir string, migrationID int64) string { + return path.Join(backupDir, fmt.Sprintf("user-migration-%d.tar.gz", migrationID)) +} + +func getLocalOrgMigrationFilepath(backupDir, org string, migrationID int64) string { + return path.Join(backupDir, fmt.Sprintf("%s-migration-%d.tar.gz", org, migrationID)) +} + func createGithubUserMigration(ctx context.Context, client interface{}, repos []*Repository, retry bool, maxNumRetries int) (*github.UserMigration, error) { var m *github.UserMigration var err error @@ -68,107 +90,98 @@ func createGithubOrgMigration(ctx context.Context, client interface{}, org strin return m, err } -func downloadGithubUserMigrationData(client interface{}, backupDir string, id *int64) { +func downloadGithubUserMigrationData( + ctx context.Context, client interface{}, backupDir string, id *int64, migrationStatePollingDuration time.Duration, +) error { var ms *github.UserMigration - ctx := context.Background() ms, _, err := client.(*github.Client).Migrations.UserMigrationStatus(ctx, *id) if err != nil { - panic(err) + return err } for { - - if *ms.State == "failed" { - log.Fatal("Migration failed.") - } - if *ms.State == "exported" { + switch *ms.State { + case migrationStateFailed: + return errors.New("migration failed") + case migrationStateExported: archiveURL, err := client.(*github.Client).Migrations.UserMigrationArchiveURL(ctx, *ms.ID) if err != nil { - panic(err) + return err } - - archiveFilepath := path.Join(backupDir, fmt.Sprintf("user-migration-%d.tar.gz", *ms.ID)) + archiveFilepath := getLocalMigrationFilepath(backupDir, *ms.ID) log.Printf("Downloading file to: %s\n", archiveFilepath) - resp, err := http.Get(archiveURL) if err != nil { - log.Fatal(err) + return fmt.Errorf("error downloading archive:%v", err) } defer resp.Body.Close() out, err := os.Create(archiveFilepath) if err != nil { - log.Fatal(err) + return err } defer out.Close() _, err = io.Copy(out, resp.Body) if err != nil { - log.Fatal(err) + return err } - break - } else { - log.Printf("Waiting for migration state to be exported: %v\n", ms.State) - time.Sleep(60 * time.Second) + return nil + default: + log.Printf("Waiting for migration state to be exported: %s\n", *ms.State) + time.Sleep(migrationStatePollingDuration) ms, _, err = client.(*github.Client).Migrations.UserMigrationStatus(ctx, *ms.ID) if err != nil { - panic(err) + return err } } } } -func downloadGithubOrgMigrationData(client interface{}, org string, backupDir string, id *int64) { - +func downloadGithubOrgMigrationData( + ctx context.Context, client interface{}, org string, backupDir string, id *int64, migrationStatePollingDuration time.Duration, +) error { var ms *github.Migration - ctx := context.Background() - ms, _, err := client.(*github.Client).Migrations.MigrationStatus(ctx, org, *id) if err != nil { - panic(err) + return err } for { - - if *ms.State == "failed" { - log.Fatal("Migration failed.") - } - if *ms.State == "exported" { + switch *ms.State { + case migrationStateFailed: + return errors.New("org migration failed") + case migrationStateExported: archiveURL, err := client.(*github.Client).Migrations.MigrationArchiveURL(ctx, org, *ms.ID) if err != nil { - panic(err) + return err } - archiveFilepath := path.Join(backupDir, fmt.Sprintf("%s-migration-%d.tar.gz", org, *ms.ID)) + archiveFilepath := getLocalOrgMigrationFilepath(backupDir, org, *ms.ID) log.Printf("Downloading file to: %s\n", archiveFilepath) resp, err := http.Get(archiveURL) if err != nil { - log.Fatal(err) + return fmt.Errorf("error downloading archive:%v", err) } defer resp.Body.Close() - out, err := os.Create(archiveFilepath) if err != nil { - log.Fatal(err) + return err } defer out.Close() _, err = io.Copy(out, resp.Body) - if err != nil { - log.Fatal(err) - } - break - } else { - log.Printf("Waiting for migration state to be exported: %v\n", ms.State) - time.Sleep(60 * time.Second) - + return err + default: + log.Printf("Waiting for migration state to be exported: %s\n", *ms.State) + time.Sleep(migrationStatePollingDuration) ms, _, err = client.(*github.Client).Migrations.MigrationStatus(ctx, org, *ms.ID) if err != nil { - panic(err) + return err } } } @@ -226,69 +239,62 @@ func DeleteGithubUserMigration(id *int64) GithubUserMigrationDeleteResult { result := GithubUserMigrationDeleteResult{} result.GhStatusCode = response.StatusCode - if err != nil { result.GhResponseBody = err.Error() - } else { - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - panic(err) - } - result.GhResponseBody = string(data) + return result } + data, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + result.GhResponseBody = string(data) return result } -func getUserOwnedOrgs(client interface{}) ([]*github.Organization, error) { +func getGithubUserOwnedOrgs(ctx context.Context, client interface{}) ([]*github.Organization, error) { var ownedOrgs []*github.Organization - ctx := context.Background() opts := github.ListOrgMembershipsOptions{State: "active"} mShips, _, err := client.(*github.Client).Organizations.ListOrgMemberships(ctx, &opts) - //TODO - if the user doesn't belong to any org, what happens? if err != nil { return nil, err } for _, m := range mShips { - if *m.Role == "admin" { + if *m.Role == orgRoleAdmin { ownedOrgs = append(ownedOrgs, m.Organization) } } return ownedOrgs, nil } -func getGithubOrgRepositories(client interface{}, o *github.Organization) ([]*Repository, error) { +func getGithubOrgRepositories(ctx context.Context, client interface{}, o *github.Organization) ([]*Repository, error) { var repositories []*Repository var cloneURL string - ctx := context.Background() // TODO: Allow customization for org repo types options := github.RepositoryListByOrgOptions{} for { // Login seems to be the safer attribute to use than organization Name repos, resp, err := client.(*github.Client).Repositories.ListByOrg(ctx, *o.Login, &options) - if err == nil { - for _, repo := range repos { - namespace := strings.Split(*repo.FullName, "/")[0] - if useHTTPSClone != nil && *useHTTPSClone { - cloneURL = *repo.CloneURL - } else { - cloneURL = *repo.SSHURL - } - repositories = append(repositories, &Repository{CloneURL: cloneURL, Name: *repo.Name, Namespace: namespace, Private: *repo.Private}) - } - } else { + if err != nil { return nil, err } + for _, repo := range repos { + namespace := strings.Split(*repo.FullName, "/")[0] + if useHTTPSClone != nil && *useHTTPSClone { + cloneURL = *repo.CloneURL + } else { + cloneURL = *repo.SSHURL + } + repositories = append(repositories, &Repository{CloneURL: cloneURL, Name: *repo.Name, Namespace: namespace, Private: *repo.Private}) + } if resp.NextPage == 0 { break } options.ListOptions.Page = resp.NextPage - } return repositories, nil } diff --git a/user_migration_test.go b/user_migration_test.go index b2e2e9c..164d207 100644 --- a/user_migration_test.go +++ b/user_migration_test.go @@ -1,10 +1,16 @@ package main import ( + "bytes" "context" + "io" "net/http" + "net/http/httptest" + "os" + "strings" "sync" "testing" + "time" "github.com/google/go-github/v34/github" githubmock "github.com/migueleliasweb/go-github-mock/src/mock" @@ -96,3 +102,111 @@ func TestCreateGitHubUserMigrationFailOnceThenSucceed(t *testing.T) { t.Fatalf("Expected to send %d requests, sent: %d\n", defaultMaxUserMigrationRetry+1, requestCounter.cnt) } } + +func TestDownloadGithubUserMigrationDataFailed(t *testing.T) { + var testMigrationID int64 = 10021 + backupDir := t.TempDir() + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetUserMigrationsByMigrationId, + github.UserMigration{ + ID: &testMigrationID, + State: &migrationStatePending, + }, + github.UserMigration{ + ID: &testMigrationID, + State: &migrationStateFailed, + }, + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + err := downloadGithubUserMigrationData(ctx, c, backupDir, &testMigrationID, 10*time.Millisecond) + if err == nil { + t.Fatalf("Expected migration download to fail.") + } +} + +func TestDownloadGithubUserMigrationDataArchiveDownloadFail(t *testing.T) { + var testMigrationID int64 = 10021 + backupDir := t.TempDir() + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetUserMigrationsByMigrationId, + github.UserMigration{ + ID: &testMigrationID, + State: &migrationStatePending, + }, + github.UserMigration{ + ID: &testMigrationID, + State: &migrationStateExported, + }, + ), + githubmock.WithRequestMatchHandler( + githubmock.GetUserMigrationsArchiveByMigrationId, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "http://127.0.0.1:8080/testarchive.tar.gz", http.StatusTemporaryRedirect) + }), + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + err := downloadGithubUserMigrationData(ctx, c, backupDir, &testMigrationID, 10*time.Millisecond) + if err == nil { + t.Fatalf("Expected migration archive download to fail.") + } + if !strings.HasPrefix(err.Error(), "error downloading archive") { + t.Fatalf("Expected error message to start with: error downloading archive, got: %v", err) + } +} + +func TestDownloadGithubUserMigrationDataArchiveDownload(t *testing.T) { + var testMigrationID int64 = 10021 + backupDir := t.TempDir() + + mux := http.NewServeMux() + mux.HandleFunc("/testarchive.tar.gz", func(w http.ResponseWriter, r *http.Request) { + b := bytes.NewBuffer([]byte("testdata")) + r.Header.Set("Content-Type", "application/gzip") + io.Copy(w, b) + }) + + ts := httptest.NewServer(mux) + defer ts.Close() + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetUserMigrationsByMigrationId, + github.UserMigration{ + ID: &testMigrationID, + State: &migrationStatePending, + }, + github.UserMigration{ + ID: &testMigrationID, + State: &migrationStateExported, + }, + ), + githubmock.WithRequestMatchHandler( + githubmock.GetUserMigrationsArchiveByMigrationId, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, ts.URL+"/testarchive.tar.gz", http.StatusTemporaryRedirect) + }), + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + err := downloadGithubUserMigrationData(ctx, c, backupDir, &testMigrationID, 10*time.Millisecond) + if err != nil { + t.Fatalf("Expected migration archive download to succeed.") + } + archiveFilepath := getLocalMigrationFilepath(backupDir, testMigrationID) + _, err = os.Stat(archiveFilepath) + if err != nil { + t.Fatalf("Expected %s to exist", archiveFilepath) + } +} diff --git a/user_org_migration_test.go b/user_org_migration_test.go new file mode 100644 index 0000000..818d6f9 --- /dev/null +++ b/user_org_migration_test.go @@ -0,0 +1,272 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/google/go-github/v34/github" + githubmock "github.com/migueleliasweb/go-github-mock/src/mock" +) + +func TestGetUserOwnedOrganizationsNone(t *testing.T) { + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetUserMembershipsOrgs, + []github.Membership{}, + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + orgs, err := getGithubUserOwnedOrgs(ctx, c) + if err != nil { + t.Fatalf("Expected to query user organizations successfully, got: %v", err) + } + if len(orgs) != 0 { + t.Fatalf("Expected slice of length 0, got %v", orgs) + } +} + +func TestGetUserOwnedOrganizations(t *testing.T) { + + testOrgNames := []string{ + "test-org-1", + "test-org-2", + "test-org-3", + } + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetUserMembershipsOrgs, + []github.Membership{ + { + Organization: &github.Organization{ + Name: &testOrgNames[0], + }, + Role: &orgRoleAdmin, + }, + { + Organization: &github.Organization{ + Name: &testOrgNames[1], + }, + Role: &orgRoleMember, + }, + { + Organization: &github.Organization{ + Name: &testOrgNames[2], + }, + Role: &orgRoleMaintainer, + }, + }, + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + orgs, err := getGithubUserOwnedOrgs(ctx, c) + if err != nil { + t.Fatalf("Expected to query user organizations successfully, got: %v", err) + } + if len(orgs) != 1 { + t.Fatalf("Expected slice of length 0, got %#v", orgs) + } + if *orgs[0].Name != testOrgNames[0] { + t.Fatalf("Expected owned organization returned to be %s, got %s", testOrgNames[0], *orgs[0].Name) + } +} + +func TestGetGithubOrganizationRepositories(t *testing.T) { + + testOrgName := "test org 1" + testOrgLogin := "test-org-1" + testRepoName := "test-repo-1" + testRepoFullname := fmt.Sprintf("%s/%s", testOrgLogin, testRepoName) + testRepoHTTPSCloneURL := "https://github.com/test-org-1/test-repo-1.git" + testRepoSSHCloneURL := "git@github.com:test-org-1/test-repo-1.git" + testRepoTypePrivate := false + + testOrg := github.Organization{ + Name: &testOrgName, + Login: &testOrgLogin, + } + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetOrgsReposByOrg, + []github.Repository{ + { + Name: &testRepoName, + FullName: &testRepoFullname, + CloneURL: &testRepoHTTPSCloneURL, + SSHURL: &testRepoSSHCloneURL, + Private: &testRepoTypePrivate, + }, + }, + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + repos, err := getGithubOrgRepositories(ctx, c, &testOrg) + if err != nil { + t.Fatalf("Expected to query user organization repositories successfully, got: %v", err) + } + if len(repos) != 1 { + t.Fatalf("Expected slice of length 0, got %#v", repos) + } + if repos[0].Name != testRepoName { + t.Fatalf("Expected returned repo name to be %s, got %s", testRepoName, repos[0].Name) + } + if repos[0].Private != testRepoTypePrivate { + t.Fatalf("Expected %v, got %v", testRepoTypePrivate, repos[0].Private) + } +} +func TestCreateGithubUserOrgMigration(t *testing.T) { + testOrg := "TestOrg" + testRepoName := "test-repo-1" + orgRepos := []*Repository{ + { + Name: testRepoName, + }, + } + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.PostOrgsMigrationsByOrg, + github.Migration{ + Repositories: []*github.Repository{ + { + Name: &testRepoName, + }, + }, + }, + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + _, err := createGithubOrgMigration(ctx, c, testOrg, orgRepos) + if err != nil { + t.Fatalf("Expected org migration to be successfully created, got: %v", err) + } +} + +func TestDownloadGithubUserOrgMigrationDataFailed(t *testing.T) { + var testMigrationID int64 = 10021 + backupDir := t.TempDir() + testOrg := "TestOrg" + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetOrgsMigrationsByOrgByMigrationId, + github.Migration{ + ID: &testMigrationID, + State: &migrationStatePending, + }, + github.Migration{ + ID: &testMigrationID, + State: &migrationStateFailed, + }, + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + err := downloadGithubOrgMigrationData(ctx, c, testOrg, backupDir, &testMigrationID, 10*time.Millisecond) + if err == nil { + t.Fatalf("Expected migration download to fail.") + } +} + +func TestDownloadGithubUserOrgMigrationDataArchiveDownloadFail(t *testing.T) { + var testMigrationID int64 = 10021 + backupDir := t.TempDir() + testOrg := "TestOrg" + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetOrgsMigrationsByOrgByMigrationId, + github.Migration{ + ID: &testMigrationID, + State: &migrationStatePending, + }, + github.Migration{ + ID: &testMigrationID, + State: &migrationStateExported, + }, + ), + githubmock.WithRequestMatchHandler( + githubmock.GetOrgsMigrationsArchiveByOrgByMigrationId, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "http://127.0.0.1:8080/testarchive.tar.gz", http.StatusTemporaryRedirect) + }), + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + err := downloadGithubOrgMigrationData(ctx, c, testOrg, backupDir, &testMigrationID, 10*time.Millisecond) + if err == nil { + t.Fatalf("Expected migration archive download to fail.") + } + if !strings.HasPrefix(err.Error(), "error downloading archive") { + t.Fatalf("Expected error message to start with: error downloading archive, got: %v", err) + } +} + +func TestDownloadGithubUserOrgMigrationDataArchiveDownload(t *testing.T) { + var testMigrationID int64 = 10021 + testOrg := "TestOrg" + backupDir := t.TempDir() + + mux := http.NewServeMux() + mux.HandleFunc("/testarchive.tar.gz", func(w http.ResponseWriter, r *http.Request) { + b := bytes.NewBuffer([]byte("testdata")) + r.Header.Set("Content-Type", "application/gzip") + io.Copy(w, b) + }) + + ts := httptest.NewServer(mux) + defer ts.Close() + + mockedHTTPClient := githubmock.NewMockedHTTPClient( + githubmock.WithRequestMatch( + githubmock.GetOrgsMigrationsByOrgByMigrationId, + github.Migration{ + ID: &testMigrationID, + State: &migrationStatePending, + }, + github.Migration{ + ID: &testMigrationID, + State: &migrationStateExported, + }, + ), + githubmock.WithRequestMatchHandler( + githubmock.GetOrgsMigrationsArchiveByOrgByMigrationId, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, ts.URL+"/testarchive.tar.gz", http.StatusTemporaryRedirect) + }), + ), + ) + + c := github.NewClient(mockedHTTPClient) + ctx := context.Background() + err := downloadGithubOrgMigrationData(ctx, c, testOrg, backupDir, &testMigrationID, 10*time.Millisecond) + if err != nil { + t.Fatalf("Expected migration archive download to succeed. Got %v", err) + } + archiveFilepath := getLocalOrgMigrationFilepath(backupDir, testOrg, testMigrationID) + _, err = os.Stat(archiveFilepath) + if err != nil { + t.Fatalf("Expected %s to exist", archiveFilepath) + } +}