gitlabhq/gitlab-shell

View on GitHub
internal/command/lfstransfer/lfstransfer_test.go

Summary

Maintainability
F
1 wk
Test Coverage
package lfstransfer

import (
    "context"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "testing"
    "time"

    "github.com/git-lfs/pktline"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/commandargs"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/command/readwriter"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
    "gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/lfsauthenticate"
)

const (
    largeFileContents      = "This is a large file\n"
    evenLargerFileContents = "This is an even larger file\n"
)

var (
    largeFileLen  = len(largeFileContents)
    largeFileHash = sha256.Sum256([]byte(largeFileContents))
    largeFileOid  = hex.EncodeToString(largeFileHash[:])

    evenLargerFileLen  = len(evenLargerFileContents)
    evenLargerFileHash = sha256.Sum256([]byte(evenLargerFileContents))
    evenLargerFileOid  = hex.EncodeToString(evenLargerFileHash[:])
)

func setupWaitGroupForExecute(t *testing.T, cmd *Command) *sync.WaitGroup {
    wg := &sync.WaitGroup{}
    wg.Add(1)

    go func() {
        _, err := cmd.Execute(context.Background())
        assert.NoError(t, err)
        wg.Done()
    }()

    return wg
}

func writeCommand(t *testing.T, pl *pktline.Pktline, command string) {
    require.NoError(t, pl.WritePacketText(command))
    require.NoError(t, pl.WriteFlush())
}

func writeCommandArgs(t *testing.T, pl *pktline.Pktline, command string, args []string) {
    require.NoError(t, pl.WritePacketText(command))
    for _, arg := range args {
        require.NoError(t, pl.WritePacketText(arg))
    }
    require.NoError(t, pl.WriteFlush())
}

func writeCommandArgsAndBinaryData(t *testing.T, pl *pktline.Pktline, command string, args []string, data [][]byte) {
    require.NoError(t, pl.WritePacketText(command))
    for _, arg := range args {
        require.NoError(t, pl.WritePacketText(arg))
    }
    require.NoError(t, pl.WriteDelim())
    for _, datum := range data {
        require.NoError(t, pl.WritePacket(datum))
    }
    require.NoError(t, pl.WriteFlush())
}

func writeCommandArgsAndTextData(t *testing.T, pl *pktline.Pktline, command string, args []string, data []string) {
    require.NoError(t, pl.WritePacketText(command))

    for _, arg := range args {
        require.NoError(t, pl.WritePacketText(arg))
    }
    require.NoError(t, pl.WriteDelim())

    for _, datum := range data {
        require.NoError(t, pl.WritePacketText(datum))
    }
    require.NoError(t, pl.WriteFlush())
}

func readCapabilities(t *testing.T, pl *pktline.Pktline) {
    var caps []string
    end := false
    for !end {
        capability, l, err := pl.ReadPacketTextWithLength()
        require.NoError(t, err)
        switch l {
        case 0:
            end = true
        case 1:
            require.Fail(t, "Expected text or flush packet, got delim packet")
        default:
            caps = append(caps, capability)
        }
    }
    require.Equal(t, []string{
        "version=1",
        "locking",
    }, caps)
}

func readStatus(t *testing.T, pl *pktline.Pktline) string {
    // Read status.
    status, l, err := pl.ReadPacketTextWithLength()
    require.NoError(t, err)
    switch l {
    case 0:
        require.Fail(t, "Expected text, got flush packet")
    case 1:
        require.Fail(t, "Expected text, got delim packet")
    }

    // Read flush.
    _, l, err = pl.ReadPacketWithLength()
    require.NoError(t, err)
    require.Zero(t, l)

    return status
}

func readStatusArgs(t *testing.T, pl *pktline.Pktline) (status string, args []string) {
    // Read status.
    status, l, err := pl.ReadPacketTextWithLength()
    require.NoError(t, err)
    switch l {
    case 0:
        require.Fail(t, "Expected text, got flush packet")
    case 1:
        require.Fail(t, "Expected text, got delim packet")
    }

    // Read args.
    end := false
    for !end {
        arg, l, err := pl.ReadPacketTextWithLength()
        require.NoError(t, err)
        switch l {
        case 0:
            end = true
        case 1:
            require.Fail(t, "Expected text or flush packet, got delim packet")
        default:
            args = append(args, arg)
        }
    }

    return status, args
}

func readStatusArgsAndBinaryData(t *testing.T, pl *pktline.Pktline) (status string, args []string, data [][]byte) {
    // Read status.
    status, l, err := pl.ReadPacketTextWithLength()
    require.NoError(t, err)
    switch l {
    case 0:
        require.Fail(t, "Expected text, got flush packet")
    case 1:
        require.Fail(t, "Expected text, got delim packet")
    }

    // Read args.
    end := false
    for !end {
        arg, l, err := pl.ReadPacketTextWithLength()
        require.NoError(t, err)
        switch l {
        case 0:
            return status, args, nil
        case 1:
            end = true
        default:
            args = append(args, arg)
        }
    }

    // Read data.
    end = false
    for !end {
        datum, l, err := pl.ReadPacketWithLength()
        require.NoError(t, err)
        switch l {
        case 0:
            end = true
        case 1:
            require.Fail(t, "Expected data or flush packet, got delim packet")
        default:
            data = append(data, datum)
        }
    }
    return status, args, data
}

func readStatusArgsAndTextData(t *testing.T, pl *pktline.Pktline) (status string, args []string, data []string) {
    // Read status.
    status, l, err := pl.ReadPacketTextWithLength()
    require.NoError(t, err)

    switch l {
    case 0:
        require.Fail(t, "Expected text, got flush packet")
    case 1:
        require.Fail(t, "Expected text, got delim packet")
    }

    // Read args.
    end := false
    for !end {
        arg, l, err := pl.ReadPacketTextWithLength()
        require.NoError(t, err)

        switch l {
        case 0:
            return status, args, nil
        case 1:
            end = true
        default:
            args = append(args, arg)
        }
    }

    // Read data.
    end = false
    for !end {
        datum, l, err := pl.ReadPacketTextWithLength()
        require.NoError(t, err)

        switch l {
        case 0:
            end = true
        case 1:
            require.Fail(t, "Expected data or flush packet, got delim packet")
        default:
            data = append(data, datum)
        }
    }

    return status, args, data
}

func negotiateVersion(t *testing.T, pl *pktline.Pktline) {
    readCapabilities(t, pl)
    writeCommand(t, pl, "version 1")
    status, _, _ := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
}

func quit(t *testing.T, pl *pktline.Pktline) {
    writeCommand(t, pl, "quit")
    status, _, _ := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
}

func TestLfsTransferCapabilities(t *testing.T) {
    _, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferNoPermissions(t *testing.T) {
    _, cmd, _, _ := setup(t, "ro", "group/repo", "upload")
    _, err := cmd.Execute(context.Background())
    require.Equal(t, "Disallowed by API call", err.Error())
}

func TestLfsTransferBatchDownload(t *testing.T) {
    url, cmd, pl, _ := setup(t, "rw", "group/repo", "download")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommandArgsAndTextData(t, pl, "batch", nil, []string{
        "00000000 0",
        fmt.Sprintf("%s %d", largeFileOid, largeFileLen),
        fmt.Sprintf("%s %d", evenLargerFileOid, evenLargerFileLen),
    })
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, "00000000 0 noop", data[0])

    largeFileArgs := strings.Split(data[1], " ")
    require.Len(t, largeFileArgs, 5)
    require.Equal(t, largeFileOid, largeFileArgs[0])
    require.Equal(t, fmt.Sprint(largeFileLen), largeFileArgs[1])
    require.Equal(t, "download", largeFileArgs[2])

    var idArg string
    var tokenArg string
    for _, arg := range largeFileArgs[3:] {
        switch {
        case strings.HasPrefix(arg, "id="):
            idArg = arg
        case strings.HasPrefix(arg, "token="):
            tokenArg = arg
        default:
            require.Fail(t, "Unexpected batch item argument: %v", arg)
        }
    }

    idBase64, found := strings.CutPrefix(idArg, "id=")
    require.True(t, found)
    idBinary, err := base64.StdEncoding.DecodeString(idBase64)
    require.NoError(t, err)

    var id map[string]interface{}
    require.NoError(t, json.Unmarshal(idBinary, &id))
    require.Equal(t, map[string]interface{}{
        "operation": "download",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s", url, largeFileOid),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }, id)

    h := hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBase64, found := strings.CutPrefix(tokenArg, "token=")
    require.True(t, found)

    tokenBinary, err := base64.StdEncoding.DecodeString(tokenBase64)
    require.NoError(t, err)
    require.Equal(t, h.Sum(nil), tokenBinary)

    require.Equal(t, fmt.Sprintf("%s %d noop", evenLargerFileOid, evenLargerFileLen), data[2])

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferBatchUpload(t *testing.T) {
    url, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommandArgsAndTextData(t, pl, "batch", nil, []string{
        "00000000 0",
        fmt.Sprintf("%s %d", largeFileOid, largeFileLen),
        fmt.Sprintf("%s %d", evenLargerFileOid, evenLargerFileLen),
    })
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, "00000000 0 noop", data[0])

    require.Equal(t, fmt.Sprintf("%s %d noop", largeFileOid, largeFileLen), data[1])

    evenLargerFileArgs := strings.Split(data[2], " ")
    require.Len(t, evenLargerFileArgs, 5)
    require.Equal(t, evenLargerFileOid, evenLargerFileArgs[0])
    require.Equal(t, fmt.Sprint(evenLargerFileLen), evenLargerFileArgs[1])
    require.Equal(t, "upload", evenLargerFileArgs[2])

    var idArg string
    var tokenArg string
    for _, arg := range evenLargerFileArgs[3:] {
        switch {
        case strings.HasPrefix(arg, "id="):
            idArg = arg
        case strings.HasPrefix(arg, "token="):
            tokenArg = arg
        default:
            require.Fail(t, "Unexpected batch item argument: %v", arg)
        }
    }

    idBase64, found := strings.CutPrefix(idArg, "id=")
    require.True(t, found)
    idBinary, err := base64.StdEncoding.DecodeString(idBase64)
    require.NoError(t, err)
    var id map[string]interface{}
    require.NoError(t, json.Unmarshal(idBinary, &id))
    require.Equal(t, map[string]interface{}{
        "operation": "upload",
        "oid":       evenLargerFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s/%d", url, evenLargerFileOid, evenLargerFileLen),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }, id)

    h := hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBase64, found := strings.CutPrefix(tokenArg, "token=")
    require.True(t, found)
    tokenBinary, err := base64.StdEncoding.DecodeString(tokenBase64)
    require.NoError(t, err)
    require.Equal(t, h.Sum(nil), tokenBinary)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferGetObject(t *testing.T) {
    url, cmd, pl, _ := setup(t, "rw", "group/repo", "download")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommand(t, pl, "get-object 00000000")
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 400", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: missing id",
    }, data)

    writeCommandArgs(t, pl, "get-object 00000000", []string{"id=ggg"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 401", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: missing token",
    }, data)

    writeCommandArgs(t, pl, "get-object 00000000", []string{"id=ggg", "token=ggg"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 400", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid id",
    }, data)

    id := base64.StdEncoding.EncodeToString([]byte("{}"))
    writeCommandArgs(t, pl, "get-object 00000000", []string{fmt.Sprintf("id=%s", id), "token=ggg"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 400", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid token",
    }, data)

    id = base64.StdEncoding.EncodeToString([]byte("{}"))
    token := base64.StdEncoding.EncodeToString([]byte("aaa"))
    writeCommandArgs(t, pl, "get-object 00000000", []string{fmt.Sprintf("id=%s", id), fmt.Sprintf("token=%s", token)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: token hash mismatch",
    }, data)

    idJSON := map[string]interface{}{
        "operation": "download",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s", url, largeFileOid),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ := json.Marshal(idJSON)
    idBase64 := base64.StdEncoding.EncodeToString(idBinary)
    h := hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary := h.Sum(nil)
    tokenBase64 := base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgs(t, pl, fmt.Sprintf("get-object %s", largeFileOid), []string{fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)})
    status, args, binData := readStatusArgsAndBinaryData(t, pl)
    require.Equal(t, "status 200", status)
    require.Equal(t, []string{
        fmt.Sprintf("size=%d", largeFileLen),
    }, args)
    require.Equal(t, [][]byte{[]byte(largeFileContents)}, binData)

    idJSON = map[string]interface{}{
        "operation": "download",
        "oid":       evenLargerFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s", url, evenLargerFileOid),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgs(t, pl, fmt.Sprintf("get-object %s", evenLargerFileOid), []string{fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 404", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        fmt.Sprintf("object %s not found", evenLargerFileOid),
    }, data)

    idJSON = map[string]interface{}{
        "operation": "upload",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s", url, largeFileOid),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgs(t, pl, fmt.Sprintf("get-object %s", largeFileOid), []string{fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid operation",
    }, data)

    idJSON = map[string]interface{}{
        "operation": "download",
        "oid":       evenLargerFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s", url, largeFileOid),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgs(t, pl, fmt.Sprintf("get-object %s", largeFileOid), []string{fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid oid",
    }, data)

    idJSON = map[string]interface{}{
        "operation": "download",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/evil-url", url),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("evil secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgs(t, pl, fmt.Sprintf("get-object %s", largeFileOid), []string{fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: token hash mismatch",
    }, data)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferPutObject(t *testing.T) {
    url, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommandArgsAndBinaryData(t, pl, "put-object 00000000", []string{"size=0"}, nil)
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 400", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: missing id",
    }, data)

    writeCommandArgsAndBinaryData(t, pl, "put-object 00000000", []string{"size=0", "id=ggg"}, nil)
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 401", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: missing token",
    }, data)

    writeCommandArgsAndBinaryData(t, pl, "put-object 00000000", []string{"size=0", "id=ggg", "token=ggg"}, nil)
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 400", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid id",
    }, data)

    id := base64.StdEncoding.EncodeToString([]byte("{}"))
    writeCommandArgsAndBinaryData(t, pl, "put-object 00000000", []string{"size=0", fmt.Sprintf("id=%s", id), "token=ggg"}, nil)
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 400", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid token",
    }, data)

    id = base64.StdEncoding.EncodeToString([]byte("{}"))
    token := base64.StdEncoding.EncodeToString([]byte("aaa"))
    writeCommandArgsAndBinaryData(t, pl, "put-object 00000000", []string{"size=0", fmt.Sprintf("id=%s", id), fmt.Sprintf("token=%s", token)}, nil)
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: token hash mismatch",
    }, data)

    idJSON := map[string]interface{}{
        "operation": "upload",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/group/noexist/gitlab-lfs/objects/%s/%d", url, largeFileOid, largeFileLen),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ := json.Marshal(idJSON)
    idBase64 := base64.StdEncoding.EncodeToString(idBinary)
    h := hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary := h.Sum(nil)
    tokenBase64 := base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgsAndBinaryData(t, pl, fmt.Sprintf("put-object %s", largeFileOid), []string{fmt.Sprintf("size=%d", largeFileLen), fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)}, [][]byte{[]byte(largeFileContents)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 404", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: not found",
    }, data)

    idJSON = map[string]interface{}{
        "operation": "upload",
        "oid":       evenLargerFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s/%d", url, evenLargerFileOid, evenLargerFileLen),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgsAndBinaryData(t, pl, fmt.Sprintf("put-object %s", evenLargerFileOid), []string{fmt.Sprintf("size=%d", evenLargerFileLen), fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)}, [][]byte{[]byte(evenLargerFileContents)})
    status = readStatus(t, pl)
    require.Equal(t, "status 200", status)

    idJSON = map[string]interface{}{
        "operation": "download",
        "oid":       evenLargerFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s/%d", url, evenLargerFileOid, evenLargerFileLen),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgsAndBinaryData(t, pl, fmt.Sprintf("put-object %s", evenLargerFileOid), []string{fmt.Sprintf("size=%d", evenLargerFileLen), fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)}, [][]byte{[]byte(evenLargerFileContents)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid operation",
    }, data)

    idJSON = map[string]interface{}{
        "operation": "upload",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s/%d", url, evenLargerFileOid, evenLargerFileLen),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("very secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgsAndBinaryData(t, pl, fmt.Sprintf("put-object %s", evenLargerFileOid), []string{fmt.Sprintf("size=%d", evenLargerFileLen), fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)}, [][]byte{[]byte(evenLargerFileContents)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: invalid oid",
    }, data)

    idJSON = map[string]interface{}{
        "operation": "upload",
        "oid":       largeFileOid,
        "href":      fmt.Sprintf("%s/evil-url", url),
        "headers": map[string]interface{}{
            "Authorization": "Basic 1234567890",
            "Content-Type":  "application/octet-stream",
        },
    }
    idBinary, _ = json.Marshal(idJSON)
    idBase64 = base64.StdEncoding.EncodeToString(idBinary)
    h = hmac.New(sha256.New, []byte("evil secret"))
    h.Write(idBinary)
    tokenBinary = h.Sum(nil)
    tokenBase64 = base64.StdEncoding.EncodeToString(tokenBinary)
    writeCommandArgsAndBinaryData(t, pl, fmt.Sprintf("put-object %s", evenLargerFileOid), []string{fmt.Sprintf("size=%d", evenLargerFileLen), fmt.Sprintf("id=%s", idBase64), fmt.Sprintf("token=%s", tokenBase64)}, [][]byte{[]byte(evenLargerFileContents)})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: token hash mismatch",
    }, data)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferVerifyObject(t *testing.T) {
    _, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommandArgs(t, pl, "verify-object 00000000", []string{"size=0"})
    status := readStatus(t, pl)
    require.Equal(t, "status 200", status)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferLock(t *testing.T) {
    _, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommandArgs(t, pl, "lock", []string{"path=/large/file/1"})
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 409", status)
    require.Equal(t, []string{
        "id=lock1",
        "path=/large/file/1",
        "locked-at=2023-10-03T13:56:20Z",
        "ownername=johndoe",
    }, args)
    require.Equal(t, []string{
        "conflict",
    }, data)

    writeCommandArgs(t, pl, "lock", []string{"path=/large/file/2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: forbidden",
    }, data)

    writeCommandArgs(t, pl, "lock", []string{"path=/large/file/3"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 500", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "internal error",
    }, data)

    writeCommandArgs(t, pl, "lock", []string{"path=/large/file/4"})
    status, args = readStatusArgs(t, pl)
    require.Equal(t, "status 201", status)
    require.Equal(t, []string{
        "id=lock4",
        "path=/large/file/4",
        "locked-at=2023-10-03T13:56:20Z",
        "ownername=johndoe",
    }, args)

    writeCommandArgs(t, pl, "lock", []string{"path=/large/file/5", "refname=refs/heads/main"})
    status, args = readStatusArgs(t, pl)
    require.Equal(t, "status 201", status)
    require.Equal(t, []string{
        "id=lock5",
        "path=/large/file/5",
        "locked-at=2023-10-03T13:56:20Z",
        "ownername=johndoe",
    }, args)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferUnlock(t *testing.T) {
    _, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommandArgs(t, pl, "unlock lock1", []string{"refname=refs/heads/main"})
    status, args := readStatusArgs(t, pl)
    require.Equal(t, "status 200", status)
    require.Equal(t, []string{
        "id=lock1",
        "path=/large/file/1",
        "locked-at=2023-10-03T13:56:20Z",
        "ownername=johndoe",
    }, args)

    writeCommandArgs(t, pl, "unlock lock2", []string{"force=true"})
    status, args = readStatusArgs(t, pl)
    require.Equal(t, "status 200", status)
    require.Equal(t, []string{
        "id=lock2",
        "path=/large/file/2",
        "locked-at=1955-11-12T22:04:00Z",
        "ownername=marty",
    }, args)

    writeCommand(t, pl, "unlock lock3")
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 403", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: forbidden",
    }, data)

    writeCommand(t, pl, "unlock lock4")
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 404", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "error: not found",
    }, data)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferListLockDownload(t *testing.T) {
    _, cmd, pl, _ := setup(t, "rw", "group/repo", "download")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommand(t, pl, "list-lock")
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock1",
        "path lock1 /large/file/1",
        "locked-at lock1 2023-10-03T13:56:20Z",
        "ownername lock1 johndoe",

        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",

        "lock lock3",
        "path lock3 /large/file/3",
        "locked-at lock3 2023-10-03T13:56:20Z",
        "ownername lock3 janedoe",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"limit=2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Equal(t, []string{
        "next-cursor=lock3",
    }, args)
    require.Equal(t, []string{
        "lock lock1",
        "path lock1 /large/file/1",
        "locked-at lock1 2023-10-03T13:56:20Z",
        "ownername lock1 johndoe",

        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"cursor=lock2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",

        "lock lock3",
        "path lock3 /large/file/3",
        "locked-at lock3 2023-10-03T13:56:20Z",
        "ownername lock3 janedoe",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"id=lock1"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock1",
        "path lock1 /large/file/1",
        "locked-at lock1 2023-10-03T13:56:20Z",
        "ownername lock1 johndoe",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"path=/large/file/2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",
    }, data)

    quit(t, pl)
    wg.Wait()
}

func TestLfsTransferListLockUpload(t *testing.T) {
    _, cmd, pl, _ := setup(t, "rw", "group/repo", "upload")
    wg := setupWaitGroupForExecute(t, cmd)
    negotiateVersion(t, pl)

    writeCommand(t, pl, "list-lock")
    status, args, data := readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock1",
        "path lock1 /large/file/1",
        "locked-at lock1 2023-10-03T13:56:20Z",
        "ownername lock1 johndoe",
        "owner lock1 ours",

        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",
        "owner lock2 theirs",

        "lock lock3",
        "path lock3 /large/file/3",
        "locked-at lock3 2023-10-03T13:56:20Z",
        "ownername lock3 janedoe",
        "owner lock3 theirs",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"limit=2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Equal(t, []string{
        "next-cursor=lock3",
    }, args)
    require.Equal(t, []string{
        "lock lock1",
        "path lock1 /large/file/1",
        "locked-at lock1 2023-10-03T13:56:20Z",
        "ownername lock1 johndoe",
        "owner lock1 ours",

        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",
        "owner lock2 theirs",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"cursor=lock2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",
        "owner lock2 theirs",

        "lock lock3",
        "path lock3 /large/file/3",
        "locked-at lock3 2023-10-03T13:56:20Z",
        "ownername lock3 janedoe",
        "owner lock3 theirs",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"id=lock1"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock1",
        "path lock1 /large/file/1",
        "locked-at lock1 2023-10-03T13:56:20Z",
        "ownername lock1 johndoe",
        "owner lock1 ours",
    }, data)

    writeCommandArgs(t, pl, "list-lock", []string{"path=/large/file/2"})
    status, args, data = readStatusArgsAndTextData(t, pl)
    require.Equal(t, "status 200", status)
    require.Empty(t, args)
    require.Equal(t, []string{
        "lock lock2",
        "path lock2 /large/file/2",
        "locked-at lock2 1955-11-12T22:04:00Z",
        "ownername lock2 marty",
        "owner lock2 theirs",
    }, data)

    quit(t, pl)
    wg.Wait()
}

type Owner struct {
    Name string `json:"name"`
}
type LockInfo struct {
    ID       string `json:"id"`
    Path     string `json:"path"`
    LockedAt string `json:"locked_at"`
    *Owner   `json:"owner"`
}

func listLocks(cursor string, limit int, refspec string, id string, path string) (locks []*LockInfo, nextCursor string) {
    allLocks := []struct {
        Refspec string
        *LockInfo
    }{
        {
            Refspec: "main",
            LockInfo: &LockInfo{
                ID:       "lock1",
                Path:     "/large/file/1",
                LockedAt: time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
                Owner: &Owner{
                    Name: "johndoe",
                },
            },
        },
        {
            Refspec: "my-branch",
            LockInfo: &LockInfo{
                ID:       "lock2",
                Path:     "/large/file/2",
                LockedAt: time.Date(1955, 11, 12, 22, 04, 0, 0, time.UTC).Format(time.RFC3339),
                Owner: &Owner{
                    Name: "marty",
                },
            },
        },
        {
            Refspec: "",
            LockInfo: &LockInfo{
                ID:       "lock3",
                Path:     "/large/file/3",
                LockedAt: time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
                Owner: &Owner{
                    Name: "janedoe",
                },
            },
        },
    }

    for _, lock := range allLocks {
        if cursor != "" && cursor != lock.ID {
            continue
        }
        cursor = ""
        if len(locks) >= limit {
            nextCursor = lock.ID
            break
        }

        if refspec != "" && refspec != lock.Refspec {
            continue
        }
        if id != "" && id != lock.ID {
            continue
        }
        if path != "" && path != lock.Path {
            continue
        }
        locks = append(locks, lock.LockInfo)
    }
    return locks, nextCursor
}

func setup(t *testing.T, keyID string, repo string, op string) (string, *Command, *pktline.Pktline, *io.PipeReader) {
    var url string

    gitalyAddress, _ := testserver.StartGitalyServer(t, "unix")
    requests := []testserver.TestRequestHandler{
        {
            Path: "/api/v4/internal/allowed",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                var requestBody map[string]interface{}
                json.NewDecoder(r.Body).Decode(&requestBody)

                allowed := map[string]interface{}{
                    "status":      true,
                    "gl_id":       "1",
                    "gl_key_type": "key",
                    "gl_key_id":   123,
                    "gl_username": "alex-doe",
                    "gitaly": map[string]interface{}{
                        "repository": map[string]interface{}{
                            "storage_name":                     "storage_name",
                            "relative_path":                    "relative_path",
                            "git_object_directory":             "path/to/git_object_directory",
                            "git_alternate_object_directories": []string{"path/to/git_alternate_object_directory"},
                            "gl_repository":                    "group/repo",
                            "gl_project_path":                  "group/project-path",
                        },
                        "address": gitalyAddress,
                        "token":   "token",
                        "features": map[string]string{
                            "gitaly-feature-cache_invalidator":        "true",
                            "gitaly-feature-inforef_uploadpack_cache": "false",
                            "some-other-ff":                           "true",
                        },
                    },
                }
                disallowed := map[string]interface{}{
                    "status":  false,
                    "message": "Disallowed by API call",
                }

                var body map[string]interface{}
                switch {
                case requestBody["key_id"] == "rw":
                    body = allowed
                case requestBody["key_id"] == "ro" && requestBody["action"] == "git-upload-pack":
                    body = allowed
                default:
                    body = disallowed
                }
                assert.NoError(t, json.NewEncoder(w).Encode(body))
            },
        },
        {
            Path: "/api/v4/internal/lfs_authenticate",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                b, err := io.ReadAll(r.Body)
                defer r.Body.Close()
                assert.NoError(t, err)

                var request *lfsauthenticate.Request
                assert.NoError(t, json.Unmarshal(b, &request))
                if request.KeyID == "rw" {
                    body := map[string]interface{}{
                        "username":             "john",
                        "lfs_token":            "sometoken",
                        "repository_http_path": fmt.Sprintf("%s/group/repo", url),
                        "expires_in":           1800,
                    }
                    assert.NoError(t, json.NewEncoder(w).Encode(body))
                } else {
                    w.WriteHeader(http.StatusForbidden)
                }
            },
        },
        {
            Path: "/group/repo/info/lfs/objects/batch",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("john:sometoken"))), r.Header.Get("Authorization"))

                var requestBody map[string]interface{}
                json.NewDecoder(r.Body).Decode(&requestBody)

                reqObjects := requestBody["objects"].([]interface{})
                retObjects := make([]map[string]interface{}, 0)
                for _, o := range reqObjects {
                    reqObject := o.(map[string]interface{})
                    retObject := map[string]interface{}{
                        "oid": reqObject["oid"],
                    }
                    switch reqObject["oid"] {
                    case largeFileOid:
                        retObject["size"] = largeFileLen
                        if op == "download" {
                            retObject["actions"] = map[string]interface{}{
                                "download": map[string]interface{}{
                                    "href": fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s", url, largeFileOid),
                                    "header": map[string]interface{}{
                                        "Authorization": "Basic 1234567890",
                                        "Content-Type":  "application/octet-stream",
                                    },
                                },
                            }
                        }
                    case evenLargerFileOid:
                        assert.Equal(t, evenLargerFileLen, int(reqObject["size"].(float64)))
                        retObject["size"] = evenLargerFileLen
                        if op == "upload" {
                            retObject["actions"] = map[string]interface{}{
                                "upload": map[string]interface{}{
                                    "href": fmt.Sprintf("%s/group/repo/gitlab-lfs/objects/%s/%d", url, evenLargerFileOid, evenLargerFileLen),
                                    "header": map[string]interface{}{
                                        "Authorization": "Basic 1234567890",
                                        "Content-Type":  "application/octet-stream",
                                    },
                                },
                            }
                        }
                    default:
                        retObject["size"] = reqObject["size"]
                        retObject["error"] = map[string]interface{}{
                            "code":    404,
                            "message": "Not found",
                        }
                    }
                    retObjects = append(retObjects, retObject)
                }

                retBody := map[string]interface{}{
                    "objects": retObjects,
                }
                body, _ := json.Marshal(retBody)
                w.Write(body)
            },
        },
        {
            Path: "/evil-url",
            Handler: func(_ http.ResponseWriter, _ *http.Request) {
                assert.Fail(t, "An attacker accessed an evil URL")
            },
        },
        {
            Path: fmt.Sprintf("/group/repo/gitlab-lfs/objects/%s", largeFileOid),
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, "Basic 1234567890", r.Header.Get("Authorization"))
                w.Write([]byte(largeFileContents))
            },
        },
        {
            Path: fmt.Sprintf("/group/repo/gitlab-lfs/objects/%s/%d", evenLargerFileOid, evenLargerFileLen),
            Handler: func(_ http.ResponseWriter, r *http.Request) {
                assert.Equal(t, http.MethodPut, r.Method)
                assert.Equal(t, "Basic 1234567890", r.Header.Get("Authorization"))
                body, _ := io.ReadAll(r.Body)
                assert.Equal(t, []byte(evenLargerFileContents), body)
            },
        },
        {
            Path: "/group/repo/info/lfs/locks/verify",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, http.MethodPost, r.Method)
                requestJSON := &struct {
                    Cursor string `json:"cursor"`
                    Limit  int    `json:"limit"`
                    Ref    struct {
                        Name string `json:"name"`
                    } `json:"ref"`
                }{}
                assert.NoError(t, json.NewDecoder(r.Body).Decode(requestJSON))

                bodyJSON := &struct {
                    Ours       []*LockInfo `json:"ours,omitempty"`
                    Theirs     []*LockInfo `json:"theirs,omitempty"`
                    NextCursor string      `json:"next_cursor,omitempty"`
                }{}
                var locks []*LockInfo
                locks, bodyJSON.NextCursor = listLocks(requestJSON.Cursor, requestJSON.Limit, requestJSON.Ref.Name, r.URL.Query().Get("id"), r.URL.Query().Get("path"))
                for _, lock := range locks {
                    if lock.ID == "lock1" {
                        bodyJSON.Ours = append(bodyJSON.Ours, lock)
                    } else {
                        bodyJSON.Theirs = append(bodyJSON.Theirs, lock)
                    }
                }

                assert.NoError(t, json.NewEncoder(w).Encode(bodyJSON))
            },
        },
        {
            Path: "/group/repo/info/lfs/locks",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                switch r.Method {
                case http.MethodGet:
                    bodyJSON := &struct {
                        Locks      []*LockInfo `json:"locks,omitempty"`
                        NextCursor string      `json:"next_cursor,omitempty"`
                    }{}
                    limit := 100
                    if r.URL.Query().Has("limit") {
                        l, err := strconv.Atoi(r.URL.Query().Get("limit"))
                        assert.NoError(t, err)
                        limit = l
                    }
                    bodyJSON.Locks, bodyJSON.NextCursor = listLocks(r.URL.Query().Get("cursor"), limit, r.URL.Query().Get("refspec"), r.URL.Query().Get("id"), r.URL.Query().Get("path"))
                    assert.NoError(t, json.NewEncoder(w).Encode(bodyJSON))
                case http.MethodPost:
                    var body map[string]interface{}
                    reader := json.NewDecoder(r.Body)
                    reader.Decode(&body)

                    var response map[string]interface{}
                    switch body["path"] {
                    case "/large/file/1":
                        response = map[string]interface{}{
                            "lock": map[string]interface{}{
                                "id":        "lock1",
                                "path":      "/large/file/1",
                                "locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
                                "owner": map[string]interface{}{
                                    "name": "johndoe",
                                },
                            },
                            "message": "already created lock",
                        }
                        w.WriteHeader(http.StatusConflict)
                    case "/large/file/2":
                        response = map[string]interface{}{
                            "message": "no permission",
                        }
                        w.WriteHeader(http.StatusForbidden)
                    case "/large/file/4":
                        response = map[string]interface{}{
                            "lock": map[string]interface{}{
                                "id":        "lock4",
                                "path":      "/large/file/4",
                                "locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
                                "owner": map[string]interface{}{
                                    "name": "johndoe",
                                },
                            },
                        }
                        w.WriteHeader(http.StatusCreated)
                    case "/large/file/5":
                        ref := body["ref"].(map[string]interface{})
                        assert.Equal(t, "refs/heads/main", ref["name"])
                        response = map[string]interface{}{
                            "lock": map[string]interface{}{
                                "id":        "lock5",
                                "path":      "/large/file/5",
                                "locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
                                "owner": map[string]interface{}{
                                    "name": "johndoe",
                                },
                            },
                        }
                        w.WriteHeader(http.StatusCreated)
                    default:
                        response = map[string]interface{}{
                            "message": "internal error",
                        }
                        w.WriteHeader(http.StatusInternalServerError)
                    }
                    writer := json.NewEncoder(w)
                    writer.Encode(response)
                }
            },
        },
        {
            Path: "/group/repo/info/lfs/locks/lock1/unlock",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, http.MethodPost, r.Method)
                var body map[string]interface{}
                assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
                assert.Equal(t, map[string]interface{}{
                    "ref": map[string]interface{}{
                        "name": "refs/heads/main",
                    },
                    "force": false,
                }, body)

                lock := map[string]interface{}{
                    "lock": map[string]interface{}{
                        "id":        "lock1",
                        "path":      "/large/file/1",
                        "locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
                        "owner": map[string]interface{}{
                            "name": "johndoe",
                        },
                    },
                }
                writer := json.NewEncoder(w)
                writer.Encode(lock)
            },
        },
        {
            Path: "/group/repo/info/lfs/locks/lock2/unlock",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, http.MethodPost, r.Method)
                var body map[string]interface{}
                assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
                assert.Equal(t, map[string]interface{}{
                    "force": true,
                }, body)

                lock := map[string]interface{}{
                    "lock": map[string]interface{}{
                        "id":        "lock2",
                        "path":      "/large/file/2",
                        "locked_at": time.Date(1955, 11, 12, 22, 4, 0, 0, time.UTC).Format(time.RFC3339),
                        "owner": map[string]interface{}{
                            "name": "marty",
                        },
                    },
                }
                writer := json.NewEncoder(w)
                writer.Encode(lock)
            },
        },
        {
            Path: "/group/repo/info/lfs/locks/lock3/unlock",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, http.MethodPost, r.Method)
                var body map[string]interface{}
                assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
                assert.Equal(t, map[string]interface{}{
                    "force": false,
                }, body)

                lock := map[string]interface{}{
                    "message": "forbidden",
                }
                w.WriteHeader(http.StatusForbidden)
                writer := json.NewEncoder(w)
                writer.Encode(lock)
            },
        },
        {
            Path: "/group/repo/info/lfs/locks/lock4/unlock",
            Handler: func(w http.ResponseWriter, r *http.Request) {
                assert.Equal(t, http.MethodPost, r.Method)
                var body map[string]interface{}
                assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
                assert.Equal(t, map[string]interface{}{
                    "force": false,
                }, body)

                lock := map[string]interface{}{
                    "message": "not found",
                }
                w.WriteHeader(http.StatusNotFound)
                writer := json.NewEncoder(w)
                writer.Encode(lock)
            },
        },
    }

    url = testserver.StartHTTPServer(t, requests)

    inputSource, inputSink := io.Pipe()
    outputSource, outputSink := io.Pipe()
    errorSource, errorSink := io.Pipe()

    cmd := &Command{
        Config:     &config.Config{GitlabUrl: url, Secret: "very secret"},
        Args:       &commandargs.Shell{GitlabKeyID: keyID, SSHArgs: []string{"git-lfs-transfer", repo, op}},
        ReadWriter: &readwriter.ReadWriter{ErrOut: errorSink, Out: outputSink, In: inputSource},
    }
    pl := pktline.NewPktline(outputSource, inputSink)

    return url, cmd, pl, errorSource
}