gitlab-org--gitlab-foss/workhorse/gitaly_test.go

871 lines
28 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strings"
"sync"
"testing"
"time"
"github.com/golang/protobuf/jsonpb" //lint:ignore SA1019 https://gitlab.com/gitlab-org/gitlab/-/issues/324868
"github.com/golang/protobuf/proto" //lint:ignore SA1019 https://gitlab.com/gitlab-org/gitlab/-/issues/324868
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/git"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
)
func TestFailedCloneNoGitaly(t *testing.T) {
// Prepare clone directory
require.NoError(t, os.RemoveAll(scratchDir))
authBody := &api.Response{
GL_ID: "user-123",
GL_USERNAME: "username",
// This will create a failure to connect to Gitaly
GitalyServer: api.GitalyServer{Address: "unix:/nonexistent"},
}
// Prepare test server and backend
ts := testAuthServer(t, nil, nil, 200, authBody)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
// Do the git clone
cloneCmd := exec.Command("git", "clone", fmt.Sprintf("%s/%s", ws.URL, testRepo), checkoutDir)
out, err := cloneCmd.CombinedOutput()
t.Log(string(out))
require.Error(t, err, "git clone should have failed")
}
func TestGetInfoRefsProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
apiResponse := gitOkBody(t)
apiResponse.GitalyServer.Address = gitalyAddress
goodMetadata := map[string]string{
"gitaly-feature-foobar": "true",
"gitaly-feature-bazqux": "false",
}
badMetadata := map[string]string{
"bad-metadata": "is blocked",
}
features := make(map[string]string)
for k, v := range goodMetadata {
features[k] = v
}
for k, v := range badMetadata {
features[k] = v
}
apiResponse.GitalyServer.Features = features
testCases := []struct {
showAllRefs bool
gitRpc string
}{
{showAllRefs: false, gitRpc: "git-upload-pack"},
{showAllRefs: true, gitRpc: "git-upload-pack"},
{showAllRefs: false, gitRpc: "git-receive-pack"},
{showAllRefs: true, gitRpc: "git-receive-pack"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("ShowAllRefs=%v,gitRpc=%v", tc.showAllRefs, tc.gitRpc), func(t *testing.T) {
apiResponse.ShowAllRefs = tc.showAllRefs
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
gitProtocol := "fake git protocol"
resource := "/gitlab-org/gitlab-test.git/info/refs?service=" + tc.gitRpc
resp, body := httpGet(t, ws.URL+resource, map[string]string{"Git-Protocol": gitProtocol})
require.Equal(t, 200, resp.StatusCode)
bodySplit := strings.SplitN(body, "\000", 3)
require.Len(t, bodySplit, 3)
gitalyRequest := &gitalypb.InfoRefsRequest{}
require.NoError(t, jsonpb.UnmarshalString(bodySplit[0], gitalyRequest))
require.Equal(t, gitProtocol, gitalyRequest.GitProtocol)
if tc.showAllRefs {
require.Equal(t, []string{git.GitConfigShowAllRefs}, gitalyRequest.GitConfigOptions)
} else {
require.Empty(t, gitalyRequest.GitConfigOptions)
}
require.Equal(t, tc.gitRpc, bodySplit[1])
require.Equal(t, string(testhelper.GitalyInfoRefsResponseMock), bodySplit[2], "GET %q: response body", resource)
md := gitalyServer.LastIncomingMetadata
for k, v := range goodMetadata {
actual := md[k]
require.Len(t, actual, 1, "number of metadata values for %v", k)
require.Equal(t, v, actual[0], "value for %v", k)
}
for k := range badMetadata {
actual := md[k]
require.Empty(t, actual, "metadata for bad key %v", k)
}
})
}
}
func TestGetInfoRefsProxiedToGitalyInterruptedStream(t *testing.T) {
apiResponse := gitOkBody(t)
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
apiResponse.GitalyServer.Address = gitalyAddress
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
resource := "/gitlab-org/gitlab-test.git/info/refs?service=git-upload-pack"
resp, err := http.Get(ws.URL + resource)
require.NoError(t, err)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestGetInfoRefsRouting(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
apiResponse := gitOkBody(t)
apiResponse.GitalyServer.Address = "unix:" + socketPath
ts := testAuthServer(t, nil, url.Values{"service": {"git-receive-pack"}}, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
testCases := []struct {
method string
path string
status int
}{
// valid requests
{"GET", "/toplevel.git/info/refs?service=git-receive-pack", 200},
{"GET", "/toplevel.wiki.git/info/refs?service=git-receive-pack", 200},
{"GET", "/toplevel/child/project.git/info/refs?service=git-receive-pack", 200},
{"GET", "/toplevel/child/project.wiki.git/info/refs?service=git-receive-pack", 200},
{"GET", "/toplevel/child/project/snippets/123.git/info/refs?service=git-receive-pack", 200},
{"GET", "/snippets/123.git/info/refs?service=git-receive-pack", 200},
// failing due to missing service parameter
{"GET", "/foo/bar.git/info/refs", 403},
// failing due to invalid service parameter
{"GET", "/foo/bar.git/info/refs?service=git-zzz-pack", 403},
// failing due to invalid repository path
{"GET", "/.git/info/refs?service=git-receive-pack", 204},
// failing due to invalid request method
{"POST", "/toplevel.git/info/refs?service=git-receive-pack", 204},
}
for _, tc := range testCases {
t.Run(tc.path, func(t *testing.T) {
req, err := http.NewRequest(tc.method, ws.URL+tc.path, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body := string(testhelper.ReadAll(t, resp.Body))
if tc.status == 200 {
require.Equal(t, 200, resp.StatusCode)
require.Contains(t, body, "\x00", "expect response generated by test gitaly server")
} else {
require.Equal(t, tc.status, resp.StatusCode)
require.Empty(t, body, "normal request has empty response body")
}
})
}
}
func waitDone(t *testing.T, done chan struct{}) {
t.Helper()
select {
case <-done:
return
case <-time.After(10 * time.Second):
t.Fatal("time out waiting for gitaly handler to return")
}
}
func TestPostReceivePackProxiedToGitalySuccessfully(t *testing.T) {
apiResponse := gitOkBody(t)
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
apiResponse.GitalyServer.Address = "unix:" + socketPath
apiResponse.GitConfigOptions = []string{"git-config-hello=world"}
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
gitProtocol := "fake Git protocol"
resource := "/gitlab-org/gitlab-test.git/git-receive-pack"
resp := httpPost(
t,
ws.URL+resource,
map[string]string{
"Content-Type": "application/x-git-receive-pack-request",
"Git-Protocol": gitProtocol,
},
bytes.NewReader(testhelper.GitalyReceivePackResponseMock),
)
defer resp.Body.Close()
body := string(testhelper.ReadAll(t, resp.Body))
split := strings.SplitN(body, "\000", 2)
require.Len(t, split, 2)
gitalyRequest := &gitalypb.PostReceivePackRequest{}
require.NoError(t, jsonpb.UnmarshalString(split[0], gitalyRequest))
require.Equal(t, apiResponse.Repository.StorageName, gitalyRequest.Repository.StorageName)
require.Equal(t, apiResponse.Repository.RelativePath, gitalyRequest.Repository.RelativePath)
require.Equal(t, apiResponse.GL_ID, gitalyRequest.GlId)
require.Equal(t, apiResponse.GL_USERNAME, gitalyRequest.GlUsername)
require.Equal(t, apiResponse.GitConfigOptions, gitalyRequest.GitConfigOptions)
require.Equal(t, gitProtocol, gitalyRequest.GitProtocol)
require.Equal(t, 200, resp.StatusCode, "POST %q", resource)
require.Equal(t, string(testhelper.GitalyReceivePackResponseMock), split[1])
testhelper.RequireResponseHeader(t, resp, "Content-Type", "application/x-git-receive-pack-result")
}
func TestPostReceivePackProxiedToGitalyInterrupted(t *testing.T) {
apiResponse := gitOkBody(t)
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
apiResponse.GitalyServer.Address = "unix:" + socketPath
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
resource := "/gitlab-org/gitlab-test.git/git-receive-pack"
resp, err := http.Post(
ws.URL+resource,
"application/x-git-receive-pack-request",
bytes.NewReader(testhelper.GitalyReceivePackResponseMock),
)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode, "POST %q", resource)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestPostReceivePackRouting(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
apiResponse := gitOkBody(t)
apiResponse.GitalyServer.Address = "unix:" + socketPath
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
testCases := []struct {
method string
path string
contentType string
match bool
}{
{"POST", "/toplevel.git/git-receive-pack", "application/x-git-receive-pack-request", true},
{"POST", "/toplevel.wiki.git/git-receive-pack", "application/x-git-receive-pack-request", true},
{"POST", "/toplevel/child/project.git/git-receive-pack", "application/x-git-receive-pack-request", true},
{"POST", "/toplevel/child/project.wiki.git/git-receive-pack", "application/x-git-receive-pack-request", true},
{"POST", "/toplevel/child/project/snippets/123.git/git-receive-pack", "application/x-git-receive-pack-request", true},
{"POST", "/snippets/123.git/git-receive-pack", "application/x-git-receive-pack-request", true},
{"POST", "/foo/bar/git-receive-pack", "application/x-git-receive-pack-request", false},
{"POST", "/foo/bar.git/git-zzz-pack", "application/x-git-receive-pack-request", false},
{"POST", "/.git/git-receive-pack", "application/x-git-receive-pack-request", false},
{"POST", "/toplevel.git/git-receive-pack", "application/x-git-upload-pack-request", false},
{"GET", "/toplevel.git/git-receive-pack", "application/x-git-receive-pack-request", false},
}
for _, tc := range testCases {
t.Run(tc.path, func(t *testing.T) {
req, err := http.NewRequest(
tc.method,
ws.URL+tc.path,
bytes.NewReader(testhelper.GitalyReceivePackResponseMock),
)
require.NoError(t, err)
req.Header.Set("Content-Type", tc.contentType)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body := string(testhelper.ReadAll(t, resp.Body))
if tc.match {
require.Equal(t, 200, resp.StatusCode)
require.Contains(t, body, "\x00", "expect response generated by test gitaly server")
} else {
require.Equal(t, 204, resp.StatusCode)
require.Empty(t, body, "normal request has empty response body")
}
})
}
}
// ReaderFunc is an adapter to turn a conforming function into an io.Reader.
type ReaderFunc func(b []byte) (int, error)
func (r ReaderFunc) Read(b []byte) (int, error) { return r(b) }
func TestPostUploadPackProxiedToGitalySuccessfully(t *testing.T) {
apiResponse := gitOkBody(t)
for i, tc := range []struct {
showAllRefs bool
code codes.Code
}{
{true, codes.OK},
{true, codes.Unavailable},
{false, codes.OK},
{false, codes.Unavailable},
} {
t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) {
apiResponse.ShowAllRefs = tc.showAllRefs
gitalyServer, socketPath := startGitalyServer(t, tc.code)
defer gitalyServer.GracefulStop()
apiResponse.GitalyServer.Address = "unix:" + socketPath
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
gitProtocol := "fake git protocol"
resource := "/gitlab-org/gitlab-test.git/git-upload-pack"
requestReader := bytes.NewReader(testhelper.GitalyUploadPackResponseMock)
var m sync.Mutex
requestReadFinished := false
resp := httpPost(
t,
ws.URL+resource,
map[string]string{
"Content-Type": "application/x-git-upload-pack-request",
"Git-Protocol": gitProtocol,
},
ReaderFunc(func(b []byte) (int, error) {
n, err := requestReader.Read(b)
if err != nil {
m.Lock()
requestReadFinished = true
m.Unlock()
}
return n, err
}),
)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode, "POST %q", resource)
testhelper.RequireResponseHeader(t, resp, "Content-Type", "application/x-git-upload-pack-result")
m.Lock()
requestFinished := requestReadFinished
m.Unlock()
require.True(t, requestFinished, "response written before request was fully read")
body := string(testhelper.ReadAll(t, resp.Body))
bodySplit := strings.SplitN(body, "\000", 2)
require.Len(t, bodySplit, 2)
gitalyRequest := &gitalypb.PostUploadPackRequest{}
require.NoError(t, jsonpb.UnmarshalString(bodySplit[0], gitalyRequest))
require.Equal(t, apiResponse.Repository.StorageName, gitalyRequest.Repository.StorageName)
require.Equal(t, apiResponse.Repository.RelativePath, gitalyRequest.Repository.RelativePath)
require.Equal(t, gitProtocol, gitalyRequest.GitProtocol)
if tc.showAllRefs {
require.Equal(t, []string{git.GitConfigShowAllRefs}, gitalyRequest.GitConfigOptions)
} else {
require.Empty(t, gitalyRequest.GitConfigOptions)
}
require.Equal(t, string(testhelper.GitalyUploadPackResponseMock), bodySplit[1], "POST %q: response body", resource)
})
}
}
func TestPostUploadPackProxiedToGitalyInterrupted(t *testing.T) {
apiResponse := gitOkBody(t)
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
apiResponse.GitalyServer.Address = "unix:" + socketPath
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
resource := "/gitlab-org/gitlab-test.git/git-upload-pack"
resp, err := http.Post(
ws.URL+resource,
"application/x-git-upload-pack-request",
bytes.NewReader(testhelper.GitalyUploadPackResponseMock),
)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode, "POST %q", resource)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestPostUploadPackRouting(t *testing.T) {
apiResponse := gitOkBody(t)
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
apiResponse.GitalyServer.Address = "unix:" + socketPath
ts := testAuthServer(t, nil, nil, 200, apiResponse)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
defer ws.Close()
testCases := []struct {
method string
path string
contentType string
match bool
}{
{"POST", "/toplevel.git/git-upload-pack", "application/x-git-upload-pack-request", true},
{"POST", "/toplevel.wiki.git/git-upload-pack", "application/x-git-upload-pack-request", true},
{"POST", "/toplevel/child/project.git/git-upload-pack", "application/x-git-upload-pack-request", true},
{"POST", "/toplevel/child/project.wiki.git/git-upload-pack", "application/x-git-upload-pack-request", true},
{"POST", "/toplevel/child/project/snippets/123.git/git-upload-pack", "application/x-git-upload-pack-request", true},
{"POST", "/snippets/123.git/git-upload-pack", "application/x-git-upload-pack-request", true},
{"POST", "/foo/bar/git-upload-pack", "application/x-git-upload-pack-request", false},
{"POST", "/foo/bar.git/git-zzz-pack", "application/x-git-upload-pack-request", false},
{"POST", "/.git/git-upload-pack", "application/x-git-upload-pack-request", false},
{"POST", "/toplevel.git/git-upload-pack", "application/x-git-receive-pack-request", false},
{"GET", "/toplevel.git/git-upload-pack", "application/x-git-upload-pack-request", false},
}
for _, tc := range testCases {
t.Run(tc.path, func(t *testing.T) {
req, err := http.NewRequest(
tc.method,
ws.URL+tc.path,
bytes.NewReader(testhelper.GitalyReceivePackResponseMock),
)
require.NoError(t, err)
req.Header.Set("Content-Type", tc.contentType)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body := string(testhelper.ReadAll(t, resp.Body))
if tc.match {
require.Equal(t, 200, resp.StatusCode)
require.Contains(t, body, "\x00", "expect response generated by test gitaly server")
} else {
require.Equal(t, 204, resp.StatusCode)
require.Empty(t, body, "normal request has empty response body")
}
})
}
}
func TestGetDiffProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e"
leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab"
repoRelativePath := "foo/bar.git"
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"RawDiffRequest":"{\"repository\":{\"storageName\":\"%s\",\"relativePath\":\"%s\"},\"rightCommitId\":\"%s\",\"leftCommitId\":\"%s\"}"}`,
gitalyAddress, repoStorage, repoRelativePath, leftCommit, rightCommit)
expectedBody := testhelper.GitalyGetDiffResponseMock
resp, body, err := doSendDataRequest("/something", "git-diff", jsonParams)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
require.Equal(t, expectedBody, string(body), "GET %q: response body", resp.Request.URL)
}
func TestGetPatchProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e"
leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab"
repoRelativePath := "foo/bar.git"
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"RawPatchRequest":"{\"repository\":{\"storageName\":\"%s\",\"relativePath\":\"%s\"},\"rightCommitId\":\"%s\",\"leftCommitId\":\"%s\"}"}`,
gitalyAddress, repoStorage, repoRelativePath, leftCommit, rightCommit)
expectedBody := testhelper.GitalyGetPatchResponseMock
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)
require.Equal(t, expectedBody, string(body), "GET %q: response body", resp.Request.URL)
}
func TestGetBlobProxiedToGitalyInterruptedStream(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
oid := "54fcc214b94e78d7a41a9a8fe6d87a5e59500e51"
repoRelativePath := "foo/bar.git"
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"GetBlobRequest":{"repository":{"storage_name":"%s","relative_path":"%s"},"oid":"%s","limit":-1}}`,
gitalyAddress, repoStorage, repoRelativePath, oid)
resp, _, err := doSendDataRequest("/something", "git-blob", jsonParams)
require.NoError(t, err)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestGetArchiveProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
oid := "54fcc214b94e78d7a41a9a8fe6d87a5e59500e51"
repoRelativePath := "foo/bar.git"
archivePrefix := "repo-1"
expectedBody := testhelper.GitalyGetArchiveResponseMock
archiveLength := len(expectedBody)
testCases := []struct {
archivePath string
cacheDisabled bool
}{
{archivePath: path.Join(scratchDir, "my/path"), cacheDisabled: false},
{archivePath: "/var/empty/my/path", cacheDisabled: true},
}
for _, tc := range testCases {
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"GitalyRepository":{"storage_name":"%s","relative_path":"%s"},"ArchivePath":"%s","ArchivePrefix":"%s","CommitId":"%s","DisableCache":%v}`,
gitalyAddress, repoStorage, repoRelativePath, tc.archivePath, archivePrefix, oid, tc.cacheDisabled)
resp, body, err := doSendDataRequest("/archive.tar.gz", "git-archive", jsonParams)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL)
require.Equal(t, expectedBody, string(body), "GET %q: response body", resp.Request.URL)
require.Equal(t, archiveLength, len(body), "GET %q: body size", resp.Request.URL)
if tc.cacheDisabled {
_, err := os.Stat(tc.archivePath)
require.True(t, os.IsNotExist(err), "expected 'does not exist', got: %v", err)
} else {
cachedArchive, err := os.ReadFile(tc.archivePath)
require.NoError(t, err)
require.Equal(t, expectedBody, string(cachedArchive))
}
}
}
func TestGetArchiveProxiedToGitalyInterruptedStream(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
oid := "54fcc214b94e78d7a41a9a8fe6d87a5e59500e51"
repoRelativePath := "foo/bar.git"
archivePath := "my/path"
archivePrefix := "repo-1"
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"GitalyRepository":{"storage_name":"%s","relative_path":"%s"},"ArchivePath":"%s","ArchivePrefix":"%s","CommitId":"%s"}`,
gitalyAddress, repoStorage, repoRelativePath, path.Join(scratchDir, archivePath), archivePrefix, oid)
resp, _, err := doSendDataRequest("/archive.tar.gz", "git-archive", jsonParams)
require.NoError(t, err)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestGetDiffProxiedToGitalyInterruptedStream(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e"
leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab"
repoRelativePath := "foo/bar.git"
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"RawDiffRequest":"{\"repository\":{\"storageName\":\"%s\",\"relativePath\":\"%s\"},\"rightCommitId\":\"%s\",\"leftCommitId\":\"%s\"}"}`,
gitalyAddress, repoStorage, repoRelativePath, leftCommit, rightCommit)
resp, _, err := doSendDataRequest("/something", "git-diff", jsonParams)
require.NoError(t, err)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestGetPatchProxiedToGitalyInterruptedStream(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
repoStorage := "default"
rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e"
leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab"
repoRelativePath := "foo/bar.git"
jsonParams := fmt.Sprintf(`{"GitalyServer":{"Address":"%s","Token":""},"RawPatchRequest":"{\"repository\":{\"storageName\":\"%s\",\"relativePath\":\"%s\"},\"rightCommitId\":\"%s\",\"leftCommitId\":\"%s\"}"}`,
gitalyAddress, repoStorage, repoRelativePath, leftCommit, rightCommit)
resp, _, err := doSendDataRequest("/something", "git-format-patch", jsonParams)
require.NoError(t, err)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func TestGetSnapshotProxiedToGitalySuccessfully(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
expectedBody := testhelper.GitalyGetSnapshotResponseMock
archiveLength := len(expectedBody)
params := buildGetSnapshotParams(gitalyAddress, buildPbRepo("default", "foo/bar.git"))
resp, body, err := doSendDataRequest("/api/v4/projects/:id/snapshot", "git-snapshot", params)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "GET %q: status code", resp.Request.URL)
require.Equal(t, expectedBody, string(body), "GET %q: body", resp.Request.URL)
require.Equal(t, archiveLength, len(body), "GET %q: body size", resp.Request.URL)
testhelper.RequireResponseHeader(t, resp, "Content-Disposition", `attachment; filename="snapshot.tar"`)
testhelper.RequireResponseHeader(t, resp, "Content-Type", "application/x-tar")
testhelper.RequireResponseHeader(t, resp, "Content-Transfer-Encoding", "binary")
testhelper.RequireResponseHeader(t, resp, "Cache-Control", "private")
}
func TestGetSnapshotProxiedToGitalyInterruptedStream(t *testing.T) {
gitalyServer, socketPath := startGitalyServer(t, codes.OK)
defer gitalyServer.GracefulStop()
gitalyAddress := "unix:" + socketPath
params := buildGetSnapshotParams(gitalyAddress, buildPbRepo("default", "foo/bar.git"))
resp, _, err := doSendDataRequest("/api/v4/projects/:id/snapshot", "git-snapshot", params)
require.NoError(t, err)
// This causes the server stream to be interrupted instead of consumed entirely.
resp.Body.Close()
done := make(chan struct{})
go func() {
gitalyServer.WaitGroup.Wait()
close(done)
}()
waitDone(t, done)
}
func buildGetSnapshotParams(gitalyAddress string, repo *gitalypb.Repository) string {
msg := serializedMessage("GetSnapshotRequest", &gitalypb.GetSnapshotRequest{Repository: repo})
return buildGitalyRPCParams(gitalyAddress, msg)
}
type rpcArg struct {
k string
v interface{}
}
// Gitlab asks workhorse to perform some long-running RPCs for it by sending
// the RPC arguments (which are protobuf messages) in HTTP response headers.
// The messages are encoded to JSON objects using pbjson, The strings are then
// re-encoded to JSON strings using json. We must replicate this behaviour here
func buildGitalyRPCParams(gitalyAddress string, rpcArgs ...rpcArg) string {
built := map[string]interface{}{
"GitalyServer": map[string]string{
"Address": gitalyAddress,
"Token": "",
},
}
for _, arg := range rpcArgs {
built[arg.k] = arg.v
}
b, err := json.Marshal(interface{}(built))
if err != nil {
panic(err)
}
return string(b)
}
func buildPbRepo(storageName, relativePath string) *gitalypb.Repository {
return &gitalypb.Repository{
StorageName: storageName,
RelativePath: relativePath,
}
}
func serializedMessage(name string, arg proto.Message) rpcArg {
m := &jsonpb.Marshaler{}
str, err := m.MarshalToString(arg)
if err != nil {
panic(err)
}
return rpcArg{name, str}
}
func serializedProtoMessage(name string, arg proto.Message) rpcArg {
msg, err := proto.Marshal(arg)
if err != nil {
panic(err)
}
return rpcArg{name, base64.URLEncoding.EncodeToString(msg)}
}
type combinedServer struct {
*grpc.Server
*testhelper.GitalyTestServer
}
func startGitalyServer(t *testing.T, finalMessageCode codes.Code) (*combinedServer, string) {
socketPath := path.Join(scratchDir, fmt.Sprintf("gitaly-%d.sock", rand.Int()))
if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
server := grpc.NewServer(testhelper.WithSidechannel())
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err)
gitalyServer := testhelper.NewGitalyServer(finalMessageCode)
gitalypb.RegisterSmartHTTPServiceServer(server, gitalyServer)
gitalypb.RegisterBlobServiceServer(server, gitalyServer)
gitalypb.RegisterRepositoryServiceServer(server, gitalyServer)
gitalypb.RegisterDiffServiceServer(server, gitalyServer)
go server.Serve(listener)
return &combinedServer{Server: server, GitalyTestServer: gitalyServer}, socketPath
}