/
niceSOFT
/
git-lfs
Обзор
Документация
Войти
/
niceSOFT
/
git-lfs
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lfshttp/ssh_test.go
406 строк
10 KB
Zalan Blenessy
Implemented review comments
07 июл 2026, 17:10
07 июл 2026, 17:10
ee07727
Код
Авторство
О чём код?
package lfshttp import ( goerrors "errors" "fmt" "os/exec" "sync" "testing" "time" "github.com/git-lfs/git-lfs/v3/errors" sshp "github.com/git-lfs/git-lfs/v3/ssh" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSSHCacheResolveFromCache(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) cache.endpoints.Store("userandhost//1//path//post", &sshAuthResponse{ Href: "cache", createdAt: time.Now(), }) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "cache", res.Href) } func TestSSHCacheResolveFromCacheWithFutureExpiresAt(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) cache.endpoints.Store("userandhost//1//path//post", &sshAuthResponse{ Href: "cache", ExpiresAt: time.Now().Add(time.Duration(1) * time.Hour), createdAt: time.Now(), }) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "cache", res.Href) } func TestSSHCacheResolveFromCacheWithFutureExpiresIn(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) cache.endpoints.Store("userandhost//1//path//post", &sshAuthResponse{ Href: "cache", ExpiresIn: 60 * 60, createdAt: time.Now(), }) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "cache", res.Href) } func TestSSHCacheResolveFromCacheWithPastExpiresAt(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) cache.endpoints.Store("userandhost//1//path//post", &sshAuthResponse{ Href: "cache", ExpiresAt: time.Now().Add(time.Duration(-1) * time.Hour), createdAt: time.Now(), }) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "real", res.Href) } func TestSSHCacheResolveFromCacheWithPastExpiresIn(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) cache.endpoints.Store("userandhost//1//path//post", &sshAuthResponse{ Href: "cache", ExpiresIn: -60 * 60, createdAt: time.Now(), }) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "real", res.Href) } func TestSSHCacheResolveFromCacheWithAmbiguousExpirationInfo(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) cache.endpoints.Store("userandhost//1//path//post", &sshAuthResponse{ Href: "cache", ExpiresIn: 60 * 60, ExpiresAt: time.Now().Add(-1 * time.Hour), createdAt: time.Now(), }) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "cache", res.Href) } func TestSSHCacheResolveWithoutError(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) assertCacheLen(t, cache, 0) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "real", res.Href) assertCacheLen(t, cache, 1) val, ok := cache.endpoints.Load("userandhost//1//path//post") assert.True(t, ok) assert.NotNil(t, val) assert.Equal(t, "real", val.(*sshAuthResponse).Href) delete(ssh.responses, "userandhost") res2, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "real", res2.Href) } func TestSSHCacheResolveWithError(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh).(*sshCache) assertCacheLen(t, cache, 0) ssh.responses["userandhost"] = sshAuthResponse{Message: "resolve error", Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } res, err := cache.Resolve(e, "post") assert.NotNil(t, err) assert.Equal(t, "real", res.Href) assertCacheLen(t, cache, 0) delete(ssh.responses, "userandhost") res2, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "", res2.Href) } // countingResolver always returns the configured error and records how many // times Resolve was called. type countingResolver struct { err error calls int } func (r *countingResolver) Resolve(e Endpoint, method string) (sshAuthResponse, error) { r.calls++ return sshAuthResponse{}, r.err } func newTestClient(t *testing.T, resolver SSHResolver) *Client { t.Helper() c := NewClient(NewContext(nil, nil, nil)) c.SSH = resolver c.sshTries = 5 return c } func testSSHEndpoint() Endpoint { return Endpoint{ Url: "https://git-server.com/foo/bar.git/info/lfs", OriginalUrl: "git@git-server.com:foo/bar.git", SSHMetadata: sshp.SSHMetadata{ UserAndHost: "git@git-server.com", Path: "foo/bar.git", }, } } func TestSSHResolveFallsBackWhenAuthenticateUnavailable(t *testing.T) { resolver := &countingResolver{ err: &sshAuthenticateUnavailableError{ err: errors.New("bash: git-lfs-authenticate: command not found"), }, } c := newTestClient(t, resolver) res, err := c.sshResolveWithRetries(testSSHEndpoint(), "GET") assert.Nil(t, err) assert.NotNil(t, res) assert.Equal(t, "", res.Href) // The unavailable command should not be retried. assert.Equal(t, 1, resolver.calls) } func TestSSHResolveFailsForOtherErrors(t *testing.T) { resolver := &countingResolver{err: errors.New("permission denied")} c := newTestClient(t, resolver) _, err := c.sshResolveWithRetries(testSSHEndpoint(), "GET") assert.NotNil(t, err) // Non-fallback errors are retried sshTries+1 times. assert.Equal(t, 6, resolver.calls) } // exitErrorWithCode runs a trivial command that exits with the given status so // that tests can obtain a real *exec.ExitError. func exitErrorWithCode(t *testing.T, code int) error { t.Helper() err := exec.Command("sh", "-c", fmt.Sprintf("exit %d", code)).Run() var exitErr *exec.ExitError require.True(t, goerrors.As(err, &exitErr)) require.Equal(t, code, exitErr.ExitCode()) return err } func TestIsSSHAuthenticateUnavailable(t *testing.T) { for _, tc := range []struct { name string exitCode int stderr string want bool }{ { name: "command not found exit code", exitCode: 127, stderr: "", want: true, }, { name: "gerrit generic exit code with message", exitCode: 1, stderr: "fatal: Gerrit Code Review: git-lfs-authenticate: not found", want: true, }, { name: "bash command not found message", exitCode: 1, stderr: "bash: git-lfs-authenticate: command not found", want: true, }, { name: "zsh command not found message", exitCode: 1, stderr: "zsh: command not found: git-lfs-authenticate", want: true, }, { name: "no such file message", exitCode: 1, stderr: "git-lfs-authenticate: No such file or directory", want: true, }, { name: "unrelated not found error is not a fallback", exitCode: 1, stderr: "Repository not found", want: false, }, { name: "generic error without message is fatal", exitCode: 1, stderr: "", want: false, }, } { t.Run(tc.name, func(t *testing.T) { err := exitErrorWithCode(t, tc.exitCode) assert.Equal(t, tc.want, isSSHAuthenticateUnavailable(err, tc.stderr)) }) } } func TestIsSSHAuthenticateUnavailableNonExitError(t *testing.T) { // A failure to start the command (not an *exec.ExitError) is a genuine // error, never an "unavailable" signal, even with a matching message. err := errors.New("exec: \"ssh\": executable file not found in $PATH") assert.False(t, isSSHAuthenticateUnavailable(err, "git-lfs-authenticate: not found")) } func assertCacheLen(t *testing.T, cache *sshCache, expected int) { t.Helper() n := 0 cache.endpoints.Range(func(_, _ any) bool { n++; return true }) assert.Equal(t, expected, n) } func TestSSHCacheConcurrentResolve(t *testing.T) { ssh := newFakeResolver() cache := withSSHCache(ssh) ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", }, } // Two goroutines resolving the same endpoint concurrently is enough // for `go test -race` to detect an unprotected map access. start := make(chan struct{}) var wg sync.WaitGroup for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() <-start res, err := cache.Resolve(e, "post") assert.Nil(t, err) assert.Equal(t, "real", res.Href) }() } close(start) wg.Wait() } func newFakeResolver() *fakeResolver { return &fakeResolver{responses: make(map[string]sshAuthResponse)} } type fakeResolver struct { responses map[string]sshAuthResponse } func (r *fakeResolver) Resolve(e Endpoint, method string) (sshAuthResponse, error) { res := r.responses[e.SSHMetadata.UserAndHost] var err error if len(res.Message) > 0 { err = errors.New(res.Message) } res.createdAt = time.Now() return res, err }