summaryrefslogtreecommitdiff
path: root/workhorse/gitaly_integration_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'workhorse/gitaly_integration_test.go')
-rw-r--r--workhorse/gitaly_integration_test.go357
1 files changed, 357 insertions, 0 deletions
diff --git a/workhorse/gitaly_integration_test.go b/workhorse/gitaly_integration_test.go
new file mode 100644
index 00000000000..418d9589235
--- /dev/null
+++ b/workhorse/gitaly_integration_test.go
@@ -0,0 +1,357 @@
+// Tests in this file need access to a real Gitaly server to run. The address
+// is supplied via the GITALY_ADDRESS environment variable
+package main
+
+import (
+ "archive/tar"
+ "bufio"
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+
+ "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
+ "gitlab.com/gitlab-org/gitlab-workhorse/internal/gitaly"
+ "gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
+)
+
+var (
+ gitalyAddress string
+ jsonGitalyServer string
+)
+
+func init() {
+ gitalyAddress = os.Getenv("GITALY_ADDRESS")
+ jsonGitalyServer = fmt.Sprintf(`"GitalyServer":{"Address":"%s", "Token": ""}`, gitalyAddress)
+}
+
+func skipUnlessRealGitaly(t *testing.T) {
+ t.Log(gitalyAddress)
+ if gitalyAddress != "" {
+ return
+ }
+
+ t.Skip(`Please set GITALY_ADDRESS="..." to run Gitaly integration tests`)
+}
+
+func realGitalyAuthResponse(apiResponse *api.Response) *api.Response {
+ apiResponse.GitalyServer.Address = gitalyAddress
+
+ return apiResponse
+}
+
+func realGitalyOkBody(t *testing.T) *api.Response {
+ return realGitalyAuthResponse(gitOkBody(t))
+}
+
+func ensureGitalyRepository(t *testing.T, apiResponse *api.Response) error {
+ ctx, namespace, err := gitaly.NewNamespaceClient(context.Background(), apiResponse.GitalyServer)
+ if err != nil {
+ return err
+ }
+ ctx, repository, err := gitaly.NewRepositoryClient(ctx, apiResponse.GitalyServer)
+ if err != nil {
+ return err
+ }
+
+ // Remove the repository if it already exists, for consistency
+ rmNsReq := &gitalypb.RemoveNamespaceRequest{
+ StorageName: apiResponse.Repository.StorageName,
+ Name: apiResponse.Repository.RelativePath,
+ }
+ _, err = namespace.RemoveNamespace(ctx, rmNsReq)
+ if err != nil {
+ return err
+ }
+
+ createReq := &gitalypb.CreateRepositoryFromURLRequest{
+ Repository: &apiResponse.Repository,
+ Url: "https://gitlab.com/gitlab-org/gitlab-test.git",
+ }
+
+ _, err = repository.CreateRepositoryFromURL(ctx, createReq)
+ return err
+}
+
+func TestAllowedClone(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ // Prepare test server and backend
+ ts := testAuthServer(t, nil, nil, 200, apiResponse)
+ defer ts.Close()
+ ws := startWorkhorseServer(ts.URL)
+ defer ws.Close()
+
+ // Do the git clone
+ require.NoError(t, os.RemoveAll(scratchDir))
+ cloneCmd := exec.Command("git", "clone", fmt.Sprintf("%s/%s", ws.URL, testRepo), checkoutDir)
+ runOrFail(t, cloneCmd)
+
+ // We may have cloned an 'empty' repository, 'git log' will fail in it
+ logCmd := exec.Command("git", "log", "-1", "--oneline")
+ logCmd.Dir = checkoutDir
+ runOrFail(t, logCmd)
+}
+
+func TestAllowedShallowClone(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ // Prepare test server and backend
+ ts := testAuthServer(t, nil, nil, 200, apiResponse)
+ defer ts.Close()
+ ws := startWorkhorseServer(ts.URL)
+ defer ws.Close()
+
+ // Shallow git clone (depth 1)
+ require.NoError(t, os.RemoveAll(scratchDir))
+ cloneCmd := exec.Command("git", "clone", "--depth", "1", fmt.Sprintf("%s/%s", ws.URL, testRepo), checkoutDir)
+ runOrFail(t, cloneCmd)
+
+ // We may have cloned an 'empty' repository, 'git log' will fail in it
+ logCmd := exec.Command("git", "log", "-1", "--oneline")
+ logCmd.Dir = checkoutDir
+ runOrFail(t, logCmd)
+}
+
+func TestAllowedPush(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ // Prepare the test server and backend
+ ts := testAuthServer(t, nil, nil, 200, apiResponse)
+ defer ts.Close()
+ ws := startWorkhorseServer(ts.URL)
+ defer ws.Close()
+
+ // Perform the git push
+ pushCmd := exec.Command("git", "push", fmt.Sprintf("%s/%s", ws.URL, testRepo), fmt.Sprintf("master:%s", newBranch()))
+ pushCmd.Dir = checkoutDir
+ runOrFail(t, pushCmd)
+}
+
+func TestAllowedGetGitBlob(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ // the LICENSE file in the test repository
+ oid := "50b27c6518be44c42c4d87966ae2481ce895624c"
+ expectedBody := "The MIT License (MIT)"
+ bodyLen := 1075
+
+ jsonParams := fmt.Sprintf(
+ `{
+ %s,
+ "GetBlobRequest":{
+ "repository":{"storage_name":"%s", "relative_path":"%s"},
+ "oid":"%s",
+ "limit":-1
+ }
+ }`,
+ jsonGitalyServer, apiResponse.Repository.StorageName, apiResponse.Repository.RelativePath, oid,
+ )
+
+ resp, body, err := doSendDataRequest("/something", "git-blob", jsonParams)
+ require.NoError(t, err)
+ shortBody := string(body[:len(expectedBody)])
+
+ require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
+ require.Equal(t, expectedBody, shortBody, "GET %q: response body", resp.Request.URL)
+ testhelper.RequireResponseHeader(t, resp, "Content-Length", strconv.Itoa(bodyLen))
+ requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL)
+}
+
+func TestAllowedGetGitArchive(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ archivePath := path.Join(scratchDir, "my/path")
+ archivePrefix := "repo-1"
+
+ msg := serializedProtoMessage("GetArchiveRequest", &gitalypb.GetArchiveRequest{
+ Repository: &apiResponse.Repository,
+ CommitId: "HEAD",
+ Prefix: archivePrefix,
+ Format: gitalypb.GetArchiveRequest_TAR,
+ Path: []byte("files"),
+ })
+ jsonParams := buildGitalyRPCParams(gitalyAddress, rpcArg{"ArchivePath", archivePath}, msg)
+
+ resp, body, err := doSendDataRequest("/archive.tar", "git-archive", jsonParams)
+ require.NoError(t, err)
+ require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
+ requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL)
+
+ // Ensure the tar file is readable
+ foundEntry := false
+ tr := tar.NewReader(bytes.NewReader(body))
+ for {
+ hdr, err := tr.Next()
+ if err != nil {
+ break
+ }
+
+ if hdr.Name == archivePrefix+"/" {
+ foundEntry = true
+ break
+ }
+ }
+
+ require.True(t, foundEntry, "Couldn't find %v directory entry", archivePrefix)
+}
+
+func TestAllowedGetGitArchiveOldPayload(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ repo := apiResponse.Repository
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ archivePath := path.Join(scratchDir, "my/path")
+ archivePrefix := "repo-1"
+
+ jsonParams := fmt.Sprintf(
+ `{
+ %s,
+ "GitalyRepository":{"storage_name":"%s","relative_path":"%s"},
+ "ArchivePath":"%s",
+ "ArchivePrefix":"%s",
+ "CommitId":"%s"
+ }`,
+ jsonGitalyServer, repo.StorageName, repo.RelativePath, archivePath, archivePrefix, "HEAD",
+ )
+
+ resp, body, err := doSendDataRequest("/archive.tar", "git-archive", jsonParams)
+ require.NoError(t, err)
+ require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
+ requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL)
+
+ // Ensure the tar file is readable
+ foundEntry := false
+ tr := tar.NewReader(bytes.NewReader(body))
+ for {
+ hdr, err := tr.Next()
+ if err != nil {
+ break
+ }
+
+ if hdr.Name == archivePrefix+"/" {
+ foundEntry = true
+ break
+ }
+ }
+
+ require.True(t, foundEntry, "Couldn't find %v directory entry", archivePrefix)
+}
+
+func TestAllowedGetGitDiff(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab"
+ rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e"
+ expectedBody := "diff --git a/README.md b/README.md"
+
+ msg := serializedMessage("RawDiffRequest", &gitalypb.RawDiffRequest{
+ Repository: &apiResponse.Repository,
+ LeftCommitId: leftCommit,
+ RightCommitId: rightCommit,
+ })
+ jsonParams := buildGitalyRPCParams(gitalyAddress, msg)
+
+ resp, body, err := doSendDataRequest("/something", "git-diff", jsonParams)
+ require.NoError(t, err)
+ shortBody := string(body[:len(expectedBody)])
+
+ require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
+ require.Equal(t, expectedBody, shortBody, "GET %q: response body", resp.Request.URL)
+ requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL)
+}
+
+func TestAllowedGetGitFormatPatch(t *testing.T) {
+ skipUnlessRealGitaly(t)
+
+ // Create the repository in the Gitaly server
+ apiResponse := realGitalyOkBody(t)
+ require.NoError(t, ensureGitalyRepository(t, apiResponse))
+
+ leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab"
+ rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e"
+ msg := serializedMessage("RawPatchRequest", &gitalypb.RawPatchRequest{
+ Repository: &apiResponse.Repository,
+ LeftCommitId: leftCommit,
+ RightCommitId: rightCommit,
+ })
+ jsonParams := buildGitalyRPCParams(gitalyAddress, msg)
+
+ resp, body, err := doSendDataRequest("/something", "git-format-patch", jsonParams)
+ require.NoError(t, err)
+
+ require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
+ requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL)
+
+ requirePatchSeries(
+ t,
+ body,
+ "372ab6950519549b14d220271ee2322caa44d4eb",
+ "57290e673a4c87f51294f5216672cbc58d485d25",
+ "41ae11ba5d091d73d5de671f6fa7d1a4539e979e",
+ "742518b2be68fc750bb4c357c0df821a88113286",
+ rightCommit,
+ )
+}
+
+var extractPatchSeriesMatcher = regexp.MustCompile(`^From (\w+)`)
+
+// RequirePatchSeries takes a `git format-patch` blob, extracts the From xxxxx
+// lines and compares the SHAs to expected list.
+func requirePatchSeries(t *testing.T, blob []byte, expected ...string) {
+ t.Helper()
+ var actual []string
+ footer := make([]string, 3)
+
+ scanner := bufio.NewScanner(bytes.NewReader(blob))
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ if matches := extractPatchSeriesMatcher.FindStringSubmatch(line); len(matches) == 2 {
+ actual = append(actual, matches[1])
+ }
+ footer = []string{footer[1], footer[2], line}
+ }
+
+ require.Equal(t, strings.Join(expected, "\n"), strings.Join(actual, "\n"), "patch series")
+
+ // Check the last returned patch is complete
+ // Don't assert on the final line, it is a git version
+ require.Equal(t, "-- ", footer[0], "end of patch marker")
+}