ftp/client_test.go
Øyvind Heddeland Instefjord 99be0634ab
Add forceListHidden dial option to force the use of 'LIST -a' command (#271)
This is useful for servers that do not offer up
hidden folders/files by default when using LIST/MLSD
commands.
Setting forceListHidden to true will force the use of
the 'LIST -a' command even when MLST support has
been detected.
2022-09-04 14:43:06 -04:00

420 lines
9.0 KiB
Go

package ftp
import (
"bytes"
"fmt"
"io"
"net"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
const (
testData = "Just some text"
testDir = "mydir"
)
func TestConnPASV(t *testing.T) {
testConn(t, true)
}
func TestConnEPSV(t *testing.T) {
testConn(t, false)
}
func testConn(t *testing.T, disableEPSV bool) {
assert := assert.New(t)
mock, c := openConn(t, "127.0.0.1", DialWithTimeout(5*time.Second), DialWithDisabledEPSV(disableEPSV))
err := c.Login("anonymous", "anonymous")
assert.NoError(err)
err = c.NoOp()
assert.NoError(err)
err = c.ChangeDir("incoming")
assert.NoError(err)
dir, err := c.CurrentDir()
if assert.NoError(err) {
assert.Equal("/incoming", dir)
}
data := bytes.NewBufferString(testData)
err = c.Stor("test", data)
assert.NoError(err)
_, err = c.List(".")
assert.NoError(err)
err = c.Rename("test", "tset")
assert.NoError(err)
// Read without deadline
r, err := c.Retr("tset")
if assert.NoError(err) {
buf, err := io.ReadAll(r)
if assert.NoError(err) {
assert.Equal(testData, string(buf))
}
r.Close()
r.Close() // test we can close two times
}
// Read with deadline
r, err = c.Retr("tset")
if assert.NoError(err) {
if err := r.SetDeadline(time.Now()); err != nil {
t.Fatal(err)
}
_, err = io.ReadAll(r)
assert.ErrorContains(err, "i/o timeout")
r.Close()
}
// Read with offset
r, err = c.RetrFrom("tset", 5)
if assert.NoError(err) {
buf, err := io.ReadAll(r)
if assert.NoError(err) {
expected := testData[5:]
assert.Equal(expected, string(buf))
}
r.Close()
}
data2 := bytes.NewBufferString(testData)
err = c.Append("tset", data2)
assert.NoError(err)
// Read without deadline, after append
r, err = c.Retr("tset")
if assert.NoError(err) {
buf, err := io.ReadAll(r)
if assert.NoError(err) {
assert.Equal(testData+testData, string(buf))
}
r.Close()
}
fileSize, err := c.FileSize("magic-file")
assert.NoError(err)
assert.Equal(int64(42), fileSize)
_, err = c.FileSize("not-found")
assert.Error(err)
entry, err := c.GetEntry("magic-file")
if err != nil {
t.Error(err)
}
if entry == nil {
t.Fatal("expected entry, got nil")
}
if entry.Size != 42 {
t.Errorf("entry size %q, expected %q", entry.Size, 42)
}
if entry.Type != EntryTypeFile {
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFile)
}
if entry.Name != "magic-file" {
t.Errorf("entry name %q, expected %q", entry.Name, "magic-file")
}
entry, err = c.GetEntry("multiline-dir")
if err != nil {
t.Error(err)
}
if entry == nil {
t.Fatal("expected entry, got nil")
}
if entry.Size != 0 {
t.Errorf("entry size %q, expected %q", entry.Size, 0)
}
if entry.Type != EntryTypeFolder {
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFolder)
}
if entry.Name != "multiline-dir" {
t.Errorf("entry name %q, expected %q", entry.Name, "multiline-dir")
}
err = c.Delete("tset")
assert.NoError(err)
err = c.MakeDir(testDir)
assert.NoError(err)
err = c.ChangeDir(testDir)
assert.NoError(err)
err = c.ChangeDirToParent()
assert.NoError(err)
entries, err := c.NameList("/")
assert.NoError(err)
assert.Equal([]string{"/incoming"}, entries)
err = c.RemoveDir(testDir)
assert.NoError(err)
err = c.Logout()
assert.NoError(err)
if err = c.Quit(); err != nil {
t.Fatal(err)
}
// Wait for the connection to close
mock.Wait()
err = c.NoOp()
assert.Error(err, "should error on closed conn")
}
// TestConnect tests the legacy Connect function
func TestConnect(t *testing.T) {
mock, err := newFtpMock(t, "127.0.0.1")
if err != nil {
t.Fatal(err)
}
defer mock.Close()
c, err := Connect(mock.Addr())
if err != nil {
t.Fatal(err)
}
if err := c.Quit(); err != nil {
t.Fatal(err)
}
mock.Wait()
}
func TestTimeout(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
if c, err := DialTimeout("localhost:2121", 1*time.Second); err == nil {
_ = c.Quit()
t.Fatal("expected timeout, got nil error")
}
}
func TestWrongLogin(t *testing.T) {
mock, err := newFtpMock(t, "127.0.0.1")
if err != nil {
t.Fatal(err)
}
defer mock.Close()
c, err := DialTimeout(mock.Addr(), 5*time.Second)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := c.Quit(); err != nil {
t.Errorf("can not quit: %s", err)
}
}()
err = c.Login("zoo2Shia", "fei5Yix9")
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestDeleteDirRecur(t *testing.T) {
mock, c := openConn(t, "127.0.0.1")
err := c.RemoveDirRecur("testDir")
if err != nil {
t.Error(err)
}
if err := c.Quit(); err != nil {
t.Fatal(err)
}
// Wait for the connection to close
mock.Wait()
}
// func TestFileDeleteDirRecur(t *testing.T) {
// mock, c := openConn(t, "127.0.0.1")
// err := c.RemoveDirRecur("testFile")
// if err == nil {
// t.Fatal("expected error got nil")
// }
// if err := c.Quit(); err != nil {
// t.Fatal(err)
// }
// // Wait for the connection to close
// mock.Wait()
// }
func TestMissingFolderDeleteDirRecur(t *testing.T) {
mock, c := openConn(t, "127.0.0.1")
err := c.RemoveDirRecur("missing-dir")
if err == nil {
t.Fatal("expected error got nil")
}
if err := c.Quit(); err != nil {
t.Fatal(err)
}
// Wait for the connection to close
mock.Wait()
}
func TestListCurrentDir(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true))
_, err := c.List("")
assert.NoError(t, err)
assert.Equal(t, "LIST", mock.lastFull, "LIST must not have a trailing whitespace")
_, err = c.NameList("")
assert.NoError(t, err)
assert.Equal(t, "NLST", mock.lastFull, "NLST must not have a trailing whitespace")
err = c.Quit()
assert.NoError(t, err)
mock.Wait()
}
func TestListCurrentDirWithForceListHidden(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true), DialWithForceListHidden(true))
assert.True(t, c.options.forceListHidden)
_, err := c.List("")
assert.NoError(t, err)
assert.Equal(t, "LIST -a", mock.lastFull, "LIST -a must not have a trailing whitespace")
err = c.Quit()
assert.NoError(t, err)
mock.Wait()
}
func TestTimeUnsupported(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "no-time")
assert.False(t, c.mdtmSupported, "MDTM must NOT be supported")
assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
assert.False(t, c.IsGetTimeSupported(), "GetTime must NOT be supported")
assert.False(t, c.IsSetTimeSupported(), "SetTime must NOT be supported")
_, err := c.GetTime("file1")
assert.NotNil(t, err)
err = c.SetTime("file1", time.Now())
assert.NotNil(t, err)
assert.NoError(t, c.Quit())
mock.Wait()
}
func TestTimeStandard(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "std-time")
assert.True(t, c.mdtmSupported, "MDTM must be supported")
assert.True(t, c.mfmtSupported, "MFMT must be supported")
assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
assert.True(t, c.IsSetTimeSupported(), "SetTime must be supported")
tm, err := c.GetTime("file1")
assert.NoError(t, err)
assert.False(t, tm.IsZero(), "GetTime must return valid time")
err = c.SetTime("file1", time.Now())
assert.NoError(t, err)
assert.NoError(t, c.Quit())
mock.Wait()
}
func TestTimeVsftpdPartial(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "vsftpd")
assert.True(t, c.mdtmSupported, "MDTM must be supported")
assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
assert.False(t, c.IsSetTimeSupported(), "SetTime must NOT be supported")
tm, err := c.GetTime("file1")
assert.NoError(t, err)
assert.False(t, tm.IsZero(), "GetTime must return valid time")
err = c.SetTime("file1", time.Now())
assert.NotNil(t, err)
assert.NoError(t, c.Quit())
mock.Wait()
}
func TestTimeVsftpdFull(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "vsftpd", DialWithWritingMDTM(true))
assert.True(t, c.mdtmSupported, "MDTM must be supported")
assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
assert.True(t, c.IsSetTimeSupported(), "SetTime must be supported")
tm, err := c.GetTime("file1")
assert.NoError(t, err)
assert.False(t, tm.IsZero(), "GetTime must return valid time")
err = c.SetTime("file1", time.Now())
assert.NoError(t, err)
assert.NoError(t, c.Quit())
mock.Wait()
}
func TestDialWithDialFunc(t *testing.T) {
dialErr := fmt.Errorf("this is proof that dial function was called")
f := func(network, address string) (net.Conn, error) {
return nil, dialErr
}
_, err := Dial("bogus-address", DialWithDialFunc(f))
assert.Equal(t, dialErr, err)
}
func TestDialWithDialer(t *testing.T) {
dialerCalled := false
dialer := net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
dialerCalled = true
return nil
},
}
mock, err := newFtpMock(t, "127.0.0.1")
assert.NoError(t, err)
c, err := Dial(mock.Addr(), DialWithDialer(dialer))
assert.NoError(t, err)
assert.NoError(t, c.Quit())
assert.Equal(t, true, dialerCalled)
}