Use mock for all tests

This commit is contained in:
Julien Laffaye 2019-04-10 20:20:50 +02:00
parent 8019e67744
commit 55546487cf
7 changed files with 398 additions and 388 deletions

View File

@ -1,14 +1,10 @@
language: go language: go
dist: trusty
sudo: required sudo: required
dist: xenial
go: go:
- 1.10.x - 1.10.x
- 1.11.x - 1.11.x
env:
- FTP_SERVER=vsftpd
- FTP_SERVER=proftpd
before_install: before_install:
- sudo $TRAVIS_BUILD_DIR/.travis/prepare.sh "$FTP_SERVER"
- sudo sysctl net.ipv6.conf.lo.disable_ipv6=0 - sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
- go get github.com/mattn/goveralls - go get github.com/mattn/goveralls
- go get github.com/golang/lint/golint - go get github.com/golang/lint/golint

View File

@ -1,19 +0,0 @@
#!/bin/sh -e
case "$1" in
proftpd)
mkdir -p /etc/proftpd/conf.d/
cp $TRAVIS_BUILD_DIR/.travis/proftpd.conf /etc/proftpd/conf.d/
;;
vsftpd)
cp $TRAVIS_BUILD_DIR/.travis/vsftpd.conf /etc/vsftpd.conf
;;
*)
echo "unknown software: $1"
exit 1
esac
mkdir --mode 0777 -p /var/ftp/incoming
apt-get update -qq
apt-get install -qq "$1"

View File

@ -1,9 +0,0 @@
<Anonymous /var/ftp>
User ftp
Group nogroup
MaxClients 2
# We want clients to be able to login with "anonymous" as well as "ftp"
UserAlias anonymous ftp
RequireValidShell off
</Anonymous>

View File

@ -1,15 +0,0 @@
# Used by Travis CI
listen=NO
listen_ipv6=YES
write_enable=YES
dirmessage_enable=YES
secure_chroot_dir=/var/run/vsftpd/empty
anonymous_enable=YES
anon_root=/var/ftp
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
anon_umask=022

View File

@ -1,107 +0,0 @@
package ftp
import (
"net"
"net/textproto"
"reflect"
"strings"
"sync"
"testing"
)
type ftpMock struct {
listener net.Listener
commands []string // list of received commands
sync.WaitGroup
}
func newFtpMock(t *testing.T, addresss string) *ftpMock {
var err error
mock := &ftpMock{}
mock.listener, err = net.Listen("tcp", addresss)
if err != nil {
t.Fatal(err)
}
go func() {
// Listen for an incoming connection.
conn, err := mock.listener.Accept()
if err != nil {
t.Fatal(err)
}
mock.Add(1)
defer mock.Done()
defer conn.Close()
proto := textproto.NewConn(conn)
proto.Writer.PrintfLine("220 FTP Server ready.")
for {
command, _ := proto.ReadLine()
// Strip the arguments
if i := strings.Index(command, " "); i > 0 {
command = command[:i]
}
// Append to list of received commands
mock.commands = append(mock.commands, command)
// At least one command must have a multiline response
switch command {
case "FEAT":
proto.Writer.PrintfLine("211-Features:\r\nFEAT\r\nPASV\r\nSIZE\r\n211 End")
case "USER":
proto.Writer.PrintfLine("331 Please send your password")
case "PASS":
proto.Writer.PrintfLine("230-Hey,\r\nWelcome to my FTP\r\n230 Access granted")
case "TYPE":
proto.Writer.PrintfLine("200 Type set ok")
case "QUIT":
proto.Writer.PrintfLine("221 Goodbye.")
return
default:
t.Fatal("unknown command:", command)
}
}
}()
return mock
}
// Closes the listening socket
func (mock *ftpMock) Close() {
mock.listener.Close()
}
// ftp.mozilla.org uses multiline 220 response
func TestMultiline(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
address := "localhost:2121"
mock := newFtpMock(t, address)
defer mock.Close()
c, err := Dial(address)
if err != nil {
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous")
if err != nil {
t.Fatal(err)
}
c.Quit()
// Wait for the connection to close
mock.Wait()
expected := []string{"FEAT", "USER", "PASS", "TYPE", "QUIT"}
if !reflect.DeepEqual(mock.commands, expected) {
t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
}
}

View File

@ -23,21 +23,15 @@ func TestConnEPSV(t *testing.T) {
} }
func testConn(t *testing.T, disableEPSV bool) { func testConn(t *testing.T, disableEPSV bool) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
c, err := DialTimeout("localhost:21", 5*time.Second) mock, c := openConn(t, "127.0.0.1")
if err != nil {
t.Fatal(err)
}
if disableEPSV { if disableEPSV {
delete(c.features, "EPSV") delete(c.features, "EPSV")
c.DisableEPSV = true c.DisableEPSV = true
} }
err = c.Login("anonymous", "anonymous") err := c.Login("anonymous", "anonymous")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -52,6 +46,15 @@ func testConn(t *testing.T, disableEPSV bool) {
t.Error(err) t.Error(err)
} }
dir, err := c.CurrentDir()
if err != nil {
t.Error(err)
} else {
if dir != "/incoming" {
t.Error("Wrong dir: " + dir)
}
}
data := bytes.NewBufferString(testData) data := bytes.NewBufferString(testData)
err = c.Stor("test", data) err = c.Stor("test", data)
if err != nil { if err != nil {
@ -115,26 +118,12 @@ func testConn(t *testing.T, disableEPSV bool) {
r.Close() r.Close()
} }
fileSize, err := c.FileSize("tset") fileSize, err := c.FileSize("magic-file")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if fileSize != 14 { if fileSize != 42 {
t.Errorf("file size %q, expected %q", fileSize, 14) t.Errorf("file size %q, expected %q", fileSize, 42)
}
data = bytes.NewBufferString("")
err = c.Stor("tset", data)
if err != nil {
t.Error(err)
}
fileSize, err = c.FileSize("tset")
if err != nil {
t.Error(err)
}
if fileSize != 0 {
t.Errorf("file size %q, expected %q", fileSize, 0)
} }
_, err = c.FileSize("not-found") _, err = c.FileSize("not-found")
@ -157,15 +146,6 @@ func testConn(t *testing.T, disableEPSV bool) {
t.Error(err) t.Error(err)
} }
dir, err := c.CurrentDir()
if err != nil {
t.Error(err)
} else {
if dir != "/incoming/"+testDir {
t.Error("Wrong dir: " + dir)
}
}
err = c.ChangeDirToParent() err = c.ChangeDirToParent()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@ -195,7 +175,12 @@ func testConn(t *testing.T, disableEPSV bool) {
} }
} }
c.Quit() if err := c.Quit(); err != nil {
t.Fatal(err)
}
// Wait for the connection to close
mock.Wait()
err = c.NoOp() err = c.NoOp()
if err == nil { if err == nil {
@ -203,41 +188,23 @@ func testConn(t *testing.T, disableEPSV bool) {
} }
} }
func TestConnIPv6(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
c, err := DialTimeout("[::1]:21", 5*time.Second)
if err != nil {
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous")
if err != nil {
t.Fatal(err)
}
_, err = c.List(".")
if err != nil {
t.Error(err)
}
c.Quit()
}
// TestConnect tests the legacy Connect function // TestConnect tests the legacy Connect function
func TestConnect(t *testing.T) { func TestConnect(t *testing.T) {
if testing.Short() { mock, err := newFtpMock(t, "127.0.0.1")
t.Skip("skipping test in short mode.") if err != nil {
t.Fatal(err)
} }
defer mock.Close()
c, err := Connect("localhost:21") c, err := Connect(mock.Addr())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
c.Quit() if err := c.Quit(); err != nil {
t.Fatal(err)
}
mock.Wait()
} }
func TestTimeout(t *testing.T) { func TestTimeout(t *testing.T) {
@ -253,11 +220,13 @@ func TestTimeout(t *testing.T) {
} }
func TestWrongLogin(t *testing.T) { func TestWrongLogin(t *testing.T) {
if testing.Short() { mock, err := newFtpMock(t, "127.0.0.1")
t.Skip("skipping test in short mode.") if err != nil {
t.Fatal(err)
} }
defer mock.Close()
c, err := DialTimeout("localhost:21", 5*time.Second) c, err := DialTimeout(mock.Addr(), 5*time.Second)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -270,189 +239,49 @@ func TestWrongLogin(t *testing.T) {
} }
func TestDeleteDirRecur(t *testing.T) { func TestDeleteDirRecur(t *testing.T) {
if testing.Short() { mock, c := openConn(t, "127.0.0.1")
t.Skip("skipping test in short mode.")
} err := c.RemoveDirRecur("testDir")
c, err := DialTimeout("localhost:21", 5*time.Second)
if err != nil { if err != nil {
t.Error(err)
}
if err := c.Quit(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = c.Login("anonymous", "anonymous") // Wait for the connection to close
if err != nil { mock.Wait()
t.Fatal(err)
}
err = c.NoOp()
if err != nil {
t.Error(err)
}
err = c.ChangeDir("incoming")
if err != nil {
t.Error(err)
}
err = c.MakeDir("testDir")
if err != nil {
t.Error(err)
}
err = c.ChangeDir("testDir")
if err != nil {
t.Error(err)
}
err = c.MakeDir("anotherDir")
if err != nil {
t.Error(err)
}
data := bytes.NewBufferString("test text")
err = c.Stor("fileTest", data)
if err != nil {
t.Error(err)
}
err = c.ChangeDirToParent()
if err != nil {
t.Error(err)
}
err = c.RemoveDirRecur("testDir")
if err != nil {
t.Error(err)
}
dir, err := c.CurrentDir()
if err != nil {
t.Error(err)
} else {
if dir != "/incoming" {
t.Error("Wrong dir: " + dir)
}
}
err = c.ChangeDir("testDir")
if err == nil {
t.Fatal("expected error, got nil")
}
err = c.Logout()
if err != nil {
if protoErr := err.(*textproto.Error); protoErr != nil {
if protoErr.Code != StatusNotImplemented {
t.Error(err)
}
} else {
t.Error(err)
}
}
c.Quit()
} }
func TestFileDeleteDirRecur(t *testing.T) { // func TestFileDeleteDirRecur(t *testing.T) {
if testing.Short() { // mock, c := openConn(t, "127.0.0.1")
t.Skip("skipping test in short mode.")
}
c, err := DialTimeout("localhost:21", 5*time.Second) // err := c.RemoveDirRecur("testFile")
if err != nil { // if err == nil {
t.Fatal(err) // t.Fatal("expected error got nil")
} // }
err = c.Login("anonymous", "anonymous") // if err := c.Quit(); err != nil {
if err != nil { // t.Fatal(err)
t.Fatal(err) // }
}
err = c.ChangeDir("incoming") // // Wait for the connection to close
if err != nil { // mock.Wait()
t.Error(err) // }
}
data := bytes.NewBufferString(testData)
err = c.Stor("testFile", data)
if err != nil {
t.Error(err)
}
err = c.RemoveDirRecur("testFile")
if err == nil {
t.Fatal("expected error got nill")
}
dir, err := c.CurrentDir()
if err != nil {
t.Error(err)
} else {
if dir != "/incoming" {
t.Error("Wrong dir: " + dir)
}
}
err = c.Delete("testFile")
if err != nil {
t.Error(err)
}
err = c.Logout()
if err != nil {
if protoErr := err.(*textproto.Error); protoErr != nil {
if protoErr.Code != StatusNotImplemented {
t.Error(err)
}
} else {
t.Error(err)
}
}
c.Quit()
}
func TestMissingFolderDeleteDirRecur(t *testing.T) { func TestMissingFolderDeleteDirRecur(t *testing.T) {
if testing.Short() { mock, c := openConn(t, "127.0.0.1")
t.Skip("skipping test in short mode.")
}
c, err := DialTimeout("localhost:21", 5*time.Second) err := c.RemoveDirRecur("missing-dir")
if err != nil {
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous")
if err != nil {
t.Fatal(err)
}
err = c.ChangeDir("incoming")
if err != nil {
t.Error(err)
}
err = c.RemoveDirRecur("test")
if err == nil { if err == nil {
t.Fatal("expected error got nill") t.Fatal("expected error got nil")
} }
dir, err := c.CurrentDir() if err := c.Quit(); err != nil {
if err != nil { t.Fatal(err)
t.Error(err)
} else {
if dir != "/incoming" {
t.Error("Wrong dir: " + dir)
}
} }
err = c.Logout() // Wait for the connection to close
if err != nil { mock.Wait()
if protoErr := err.(*textproto.Error); protoErr != nil {
if protoErr.Code != StatusNotImplemented {
t.Error(err)
}
} else {
t.Error(err)
}
}
c.Quit()
} }

335
conn_test.go Normal file
View File

@ -0,0 +1,335 @@
package ftp
import (
"errors"
"io"
"io/ioutil"
"net"
"net/textproto"
"reflect"
"strconv"
"strings"
"sync"
"testing"
)
type ftpMock struct {
address string
listener *net.TCPListener
proto *textproto.Conn
commands []string // list of received commands
rest int
dataConn *mockDataConn
sync.WaitGroup
}
// newFtpMock returns a mock implementation of a FTP server
// For simplication, a mock instance only accepts a signle connection and terminates afer
func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
var err error
mock := &ftpMock{address: address}
l, err := net.Listen("tcp", address+":0")
if err != nil {
return nil, err
}
tcpListener, ok := l.(*net.TCPListener)
if !ok {
return nil, errors.New("listener is not a net.TCPListener")
}
mock.listener = tcpListener
go mock.listen(t)
return mock, nil
}
func (mock *ftpMock) listen(t *testing.T) {
// Listen for an incoming connection.
conn, err := mock.listener.Accept()
if err != nil {
t.Errorf("can not accept: %s", err)
return
}
// Do not accept incoming connections anymore
mock.listener.Close()
mock.Add(1)
defer mock.Done()
defer conn.Close()
mock.proto = textproto.NewConn(conn)
mock.proto.Writer.PrintfLine("220 FTP Server ready.")
for {
fullCommand, _ := mock.proto.ReadLine()
cmdParts := strings.Split(fullCommand, " ")
// Append to list of received commands
mock.commands = append(mock.commands, cmdParts[0])
// At least one command must have a multiline response
switch cmdParts[0] {
case "FEAT":
mock.proto.Writer.PrintfLine("211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n SIZE\r\n211 End")
case "USER":
if cmdParts[1] == "anonymous" {
mock.proto.Writer.PrintfLine("331 Please send your password")
} else {
mock.proto.Writer.PrintfLine("530 This FTP server is anonymous only")
}
case "PASS":
mock.proto.Writer.PrintfLine("230-Hey,\r\nWelcome to my FTP\r\n230 Access granted")
case "TYPE":
mock.proto.Writer.PrintfLine("200 Type set ok")
case "CWD":
if cmdParts[1] == "missing-dir" {
mock.proto.Writer.PrintfLine("550 %s: No such file or directory", cmdParts[1])
} else {
mock.proto.Writer.PrintfLine("250 Directory successfully changed.")
}
case "DELE":
mock.proto.Writer.PrintfLine("250 File successfully removed.")
case "MKD":
mock.proto.Writer.PrintfLine("257 Directory successfully created.")
case "RMD":
if cmdParts[1] == "missing-dir" {
mock.proto.Writer.PrintfLine("550 No such file or directory")
} else {
mock.proto.Writer.PrintfLine("250 Directory successfully removed.")
}
case "PWD":
mock.proto.Writer.PrintfLine("257 \"/incoming\"")
case "CDUP":
mock.proto.Writer.PrintfLine("250 CDUP command successful")
case "SIZE":
if cmdParts[1] == "magic-file" {
mock.proto.Writer.PrintfLine("213 42")
} else {
mock.proto.Writer.PrintfLine("550 Could not get file size.")
}
case "PASV":
p, err := mock.listenDataConn()
if err != nil {
mock.proto.Writer.PrintfLine("451 %s.", err)
break
}
p1 := int(p / 256)
p2 := p % 256
mock.proto.Writer.PrintfLine("227 Entering Passive Mode (127,0,0,1,%d,%d).", p1, p2)
case "EPSV":
p, err := mock.listenDataConn()
if err != nil {
mock.proto.Writer.PrintfLine("451 %s.", err)
break
}
mock.proto.Writer.PrintfLine("229 Entering Extended Passive Mode (|||%d|)", p)
case "STOR":
if mock.dataConn == nil {
mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
break
}
mock.proto.Writer.PrintfLine("150 please send")
mock.recvDataConn()
case "LIST":
if mock.dataConn == nil {
mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
break
}
mock.dataConn.Wait()
mock.proto.Writer.PrintfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.conn.Write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo"))
mock.proto.Writer.PrintfLine("226 Transfer complete")
mock.closeDataConn()
case "NLST":
if mock.dataConn == nil {
mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
break
}
mock.dataConn.Wait()
mock.proto.Writer.PrintfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.conn.Write([]byte("/incoming"))
mock.proto.Writer.PrintfLine("226 Transfer complete")
mock.closeDataConn()
case "RETR":
if mock.dataConn == nil {
mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
break
}
mock.dataConn.Wait()
mock.proto.Writer.PrintfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.conn.Write([]byte(testData[mock.rest:]))
mock.rest = 0
mock.proto.Writer.PrintfLine("226 Transfer complete")
mock.closeDataConn()
case "RNFR":
mock.proto.Writer.PrintfLine("350 File or directory exists, ready for destination name")
case "RNTO":
mock.proto.Writer.PrintfLine("250 Rename successful")
case "REST":
if len(cmdParts) != 2 {
mock.proto.Writer.PrintfLine("500 wrong number of arguments")
break
}
rest, err := strconv.Atoi(cmdParts[1])
if err != nil {
mock.proto.Writer.PrintfLine("500 REST: %s", err)
break
}
mock.rest = rest
mock.proto.Writer.PrintfLine("350 Restarting at %s. Send STORE or RETRIEVE to initiate transfer", cmdParts[1])
case "NOOP":
mock.proto.Writer.PrintfLine("200 NOOP ok.")
case "REIN":
mock.proto.Writer.PrintfLine("220 Logged out")
case "QUIT":
mock.proto.Writer.PrintfLine("221 Goodbye.")
return
default:
mock.proto.Writer.PrintfLine("500 Unknown command %s.", cmdParts[0])
}
}
}
func (mock *ftpMock) closeDataConn() (err error) {
if mock.dataConn != nil {
err = mock.dataConn.Close()
mock.dataConn = nil
}
return
}
type mockDataConn struct {
listener *net.TCPListener
conn net.Conn
// WaitGroup is done when conn is accepted and stored
sync.WaitGroup
}
func (d *mockDataConn) Close() (err error) {
if d.listener != nil {
err = d.listener.Close()
}
if d.conn != nil {
err = d.conn.Close()
}
return
}
func (mock *ftpMock) listenDataConn() (int64, error) {
mock.closeDataConn()
l, err := net.Listen("tcp", mock.address+":0")
if err != nil {
return 0, err
}
tcpListener, ok := l.(*net.TCPListener)
if !ok {
return 0, errors.New("listener is not a net.TCPListener")
}
addr := tcpListener.Addr().String()
_, port, err := net.SplitHostPort(addr)
if err != nil {
return 0, err
}
p, err := strconv.ParseInt(port, 10, 32)
if err != nil {
return 0, err
}
dataConn := &mockDataConn{listener: tcpListener}
dataConn.Add(1)
go func() {
// Listen for an incoming connection.
conn, err := dataConn.listener.Accept()
if err != nil {
// t.Errorf("can not accept: %s", err)
return
}
dataConn.conn = conn
dataConn.Done()
}()
mock.dataConn = dataConn
return p, nil
}
func (mock *ftpMock) recvDataConn() {
mock.dataConn.Wait()
io.Copy(ioutil.Discard, mock.dataConn.conn)
mock.proto.Writer.PrintfLine("226 Transfer Complete")
mock.closeDataConn()
}
func (mock *ftpMock) Addr() string {
return mock.listener.Addr().String()
}
// Closes the listening socket
func (mock *ftpMock) Close() {
mock.listener.Close()
}
// Helper to return a client connected to a mock server
func openConn(t *testing.T, addr string) (*ftpMock, *ServerConn) {
mock, err := newFtpMock(t, addr)
if err != nil {
t.Fatal(err)
}
defer mock.Close()
c, err := Dial(mock.Addr())
if err != nil {
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous")
if err != nil {
t.Fatal(err)
}
return mock, c
}
// Helper to close a client connected to a mock server
func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
expected := []string{"FEAT", "USER", "PASS", "TYPE"}
expected = append(expected, commands...)
expected = append(expected, "QUIT")
if err := c.Quit(); err != nil {
t.Fatal(err)
}
// Wait for the connection to close
mock.Wait()
if !reflect.DeepEqual(mock.commands, expected) {
t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
}
}
func TestConn4(t *testing.T) {
mock, c := openConn(t, "127.0.0.1")
closeConn(t, mock, c, nil)
}
func TestConn6(t *testing.T) {
mock, c := openConn(t, "[::1]")
closeConn(t, mock, c, nil)
}