Files
coder/agent/usershell/usershell_test.go
T
Mathias Fredriksson 3955df796e fix(agent): unify working directory resolution (#26102)
agentssh's CommandEnv, sftpHandler, and agentproc each resolved the
session working directory on their own and had drifted: sftpHandler used
the configured directory without checking it exists and bypassed the
injected EnvInfoer, while the others stat-checked and fell back to home.
Home and shell lookups could also skip the EnvInfoer seam through the
exported usershell.HomeDir and Get.

Resolve through a single usershell.ResolveWorkingDirectory and confine
host home and shell lookups to usershell, so SSH sessions, the process
API, and tests can no longer diverge and the injected environment is
always honored. This also fixes SFTP landing in a configured directory
that no longer exists.

Refs coder/coder#26099
2026-06-08 14:24:32 +03:00

154 lines
4.1 KiB
Go

package usershell_test
import (
"os"
"os/user"
"path/filepath"
"runtime"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/agent/usershell"
)
//nolint:paralleltest,tparallel // This test sets an environment variable.
func TestShell(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
ei := usershell.SystemEnvInfo{}
t.Run("Fallback", func(t *testing.T) {
t.Setenv("SHELL", "/bin/sh")
t.Run("NonExistentUser", func(t *testing.T) {
shell, err := ei.Shell("notauser")
require.NoError(t, err)
require.Equal(t, "/bin/sh", shell)
})
})
t.Run("NoFallback", func(t *testing.T) {
// Disable env fallback for these tests.
t.Setenv("SHELL", "")
t.Run("NotFound", func(t *testing.T) {
_, err := ei.Shell("notauser")
require.Error(t, err)
})
t.Run("User", func(t *testing.T) {
u, err := user.Current()
require.NoError(t, err)
shell, err := ei.Shell(u.Username)
require.NoError(t, err)
require.NotEmpty(t, shell)
})
})
t.Run("Remove GOTRACEBACK=none", func(t *testing.T) {
t.Setenv("GOTRACEBACK", "none")
env := ei.Environ()
for _, e := range env {
require.NotEqual(t, "GOTRACEBACK=none", e)
}
})
}
// homeEnvInfo reports a fixed home directory and otherwise delegates to
// SystemEnvInfo, isolating ResolveWorkingDirectory tests from the host's real
// home directory.
type homeEnvInfo struct {
usershell.SystemEnvInfo
home string
}
func (e homeEnvInfo) HomeDir() (string, error) { return e.home, nil }
// errorEnvInfo reports an error from HomeDir to exercise the fallback
// error path.
type errorEnvInfo struct {
usershell.SystemEnvInfo
err error
}
func (e errorEnvInfo) HomeDir() (string, error) { return "", e.err }
func TestResolveWorkingDirectory(t *testing.T) {
t.Parallel()
const home = "/home/coder"
ei := homeEnvInfo{home: home}
t.Run("Exists", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
require.NoError(t, fs.MkdirAll("/work", 0o700))
dir, err := usershell.ResolveWorkingDirectory(fs, ei, "/work")
require.NoError(t, err)
require.Equal(t, "/work", dir)
})
t.Run("Missing", func(t *testing.T) {
t.Parallel()
dir, err := usershell.ResolveWorkingDirectory(afero.NewMemMapFs(), ei, "/work")
require.NoError(t, err)
require.Equal(t, home, dir)
})
t.Run("Empty", func(t *testing.T) {
t.Parallel()
dir, err := usershell.ResolveWorkingDirectory(afero.NewMemMapFs(), ei, "")
require.NoError(t, err)
require.Equal(t, home, dir)
})
t.Run("NotADirectory", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "/work", []byte("file"), 0o600))
dir, err := usershell.ResolveWorkingDirectory(fs, ei, "/work")
require.NoError(t, err)
require.Equal(t, home, dir)
})
t.Run("HomeDirError", func(t *testing.T) {
t.Parallel()
ei := errorEnvInfo{err: xerrors.New("no home")}
_, err := usershell.ResolveWorkingDirectory(afero.NewMemMapFs(), ei, "")
require.ErrorContains(t, err, "no home")
})
t.Run("Symlink", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
// MemMapFs cannot model symlinks. Use the real filesystem to
// confirm Stat follows symlinks: a link to a directory is honored,
// a link to a non-directory falls back to home.
fs := afero.NewOsFs()
base := t.TempDir()
realDir := filepath.Join(base, "real")
require.NoError(t, os.Mkdir(realDir, 0o700))
linkToDir := filepath.Join(base, "link-dir")
require.NoError(t, os.Symlink(realDir, linkToDir))
dir, err := usershell.ResolveWorkingDirectory(fs, ei, linkToDir)
require.NoError(t, err)
require.Equal(t, linkToDir, dir, "symlink to a directory should be honored")
realFile := filepath.Join(base, "file")
require.NoError(t, os.WriteFile(realFile, []byte("x"), 0o600))
linkToFile := filepath.Join(base, "link-file")
require.NoError(t, os.Symlink(realFile, linkToFile))
dir, err = usershell.ResolveWorkingDirectory(fs, ei, linkToFile)
require.NoError(t, err)
require.Equal(t, home, dir, "symlink to a non-directory should fall back to home")
})
}