2016-06-13 19:37:38 +08:00
|
|
|
package sftp
|
|
|
|
|
2016-06-15 18:04:25 +08:00
|
|
|
import (
|
2020-10-30 16:36:20 +08:00
|
|
|
"bytes"
|
2018-01-27 09:26:44 +08:00
|
|
|
"io"
|
2017-08-10 13:12:50 +08:00
|
|
|
"os"
|
2018-08-21 03:39:58 +08:00
|
|
|
"path"
|
2017-08-10 13:12:50 +08:00
|
|
|
"regexp"
|
2020-09-11 00:11:47 +08:00
|
|
|
"runtime"
|
2017-08-10 13:49:14 +08:00
|
|
|
"sync"
|
2018-01-27 09:26:44 +08:00
|
|
|
"syscall"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/stretchr/testify/assert"
|
2020-10-30 16:36:20 +08:00
|
|
|
"github.com/stretchr/testify/require"
|
2017-08-10 13:12:50 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2017-08-13 20:00:08 +08:00
|
|
|
typeDirectory = "d"
|
|
|
|
typeFile = "[^d]"
|
2016-06-15 18:04:25 +08:00
|
|
|
)
|
2016-06-13 19:37:38 +08:00
|
|
|
|
2017-08-10 13:12:50 +08:00
|
|
|
func TestRunLsWithExamplesDirectory(t *testing.T) {
|
|
|
|
path := "examples"
|
2017-08-13 20:00:08 +08:00
|
|
|
item, _ := os.Stat(path)
|
2017-08-10 13:12:50 +08:00
|
|
|
result := runLs(path, item)
|
2017-08-13 20:00:08 +08:00
|
|
|
runLsTestHelper(t, result, typeDirectory, path)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestRunLsWithLicensesFile(t *testing.T) {
|
|
|
|
path := "LICENSE"
|
2017-08-13 20:00:08 +08:00
|
|
|
item, _ := os.Stat(path)
|
2017-08-10 13:12:50 +08:00
|
|
|
result := runLs(path, item)
|
2017-08-13 20:00:08 +08:00
|
|
|
runLsTestHelper(t, result, typeFile, path)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
2017-08-10 01:43:14 +08:00
|
|
|
/*
|
|
|
|
The format of the `longname' field is unspecified by this protocol.
|
|
|
|
It MUST be suitable for use in the output of a directory listing
|
|
|
|
command (in fact, the recommended operation for a directory listing
|
|
|
|
command is to simply display this data). However, clients SHOULD NOT
|
|
|
|
attempt to parse the longname field for file attributes; they SHOULD
|
|
|
|
use the attrs field instead.
|
|
|
|
|
|
|
|
The recommended format for the longname field is as follows:
|
|
|
|
|
|
|
|
-rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer
|
|
|
|
1234567890 123 12345678 12345678 12345678 123456789012
|
|
|
|
|
|
|
|
Here, the first line is sample output, and the second field indicates
|
|
|
|
widths of the various fields. Fields are separated by spaces. The
|
|
|
|
first field lists file permissions for user, group, and others; the
|
|
|
|
second field is link count; the third field is the name of the user
|
|
|
|
who owns the file; the fourth field is the name of the group that
|
|
|
|
owns the file; the fifth field is the size of the file in bytes; the
|
|
|
|
sixth field (which actually may contain spaces, but is fixed to 12
|
|
|
|
characters) is the file modification time, and the seventh field is
|
|
|
|
the file name. Each field is specified to be a minimum of certain
|
|
|
|
number of character positions (indicated by the second line above),
|
|
|
|
but may also be longer if the data does not fit in the specified
|
|
|
|
length.
|
|
|
|
|
|
|
|
The SSH_FXP_ATTRS response has the following format:
|
|
|
|
|
|
|
|
uint32 id
|
|
|
|
ATTRS attrs
|
|
|
|
|
|
|
|
where `id' is the request identifier, and `attrs' is the returned
|
|
|
|
file attributes as described in Section ``File Attributes''.
|
2018-01-27 09:26:44 +08:00
|
|
|
*/
|
2017-08-10 13:12:50 +08:00
|
|
|
func runLsTestHelper(t *testing.T, result, expectedType, path string) {
|
|
|
|
// using regular expressions to make tests work on all systems
|
|
|
|
// a virtual file system (like afero) would be needed to mock valid filesystem checks
|
|
|
|
// expected layout is:
|
|
|
|
// drwxr-xr-x 8 501 20 272 Aug 9 19:46 examples
|
2017-08-10 01:43:14 +08:00
|
|
|
|
2017-08-10 13:12:50 +08:00
|
|
|
// permissions (len 10, "drwxr-xr-x")
|
|
|
|
got := result[0:10]
|
2017-08-13 20:00:08 +08:00
|
|
|
if ok, err := regexp.MatchString("^"+expectedType+"[rwx-]{9}$", got); !ok {
|
|
|
|
t.Errorf("runLs(%#v, *FileInfo): permission field mismatch, expected dir, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// space
|
|
|
|
got = result[10:11]
|
|
|
|
if ok, err := regexp.MatchString("^\\s$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): spacer 1 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// link count (len 3, number)
|
|
|
|
got = result[12:15]
|
|
|
|
if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): link count field mismatch, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// spacer
|
|
|
|
got = result[15:16]
|
|
|
|
if ok, err := regexp.MatchString("^\\s$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): spacer 2 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// username / uid (len 8, number or string)
|
|
|
|
got = result[16:24]
|
|
|
|
if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): username / uid mismatch, expected user, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// spacer
|
|
|
|
got = result[24:25]
|
|
|
|
if ok, err := regexp.MatchString("^\\s$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): spacer 3 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// groupname / gid (len 8, number or string)
|
|
|
|
got = result[25:33]
|
|
|
|
if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): groupname / gid mismatch, expected group, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// spacer
|
|
|
|
got = result[33:34]
|
|
|
|
if ok, err := regexp.MatchString("^\\s$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): spacer 4 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// filesize (len 8)
|
|
|
|
got = result[34:42]
|
|
|
|
if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): filesize field mismatch, expected size in bytes, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// spacer
|
|
|
|
got = result[42:43]
|
|
|
|
if ok, err := regexp.MatchString("^\\s$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): spacer 5 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// mod time (len 12, e.g. Aug 9 19:46)
|
|
|
|
got = result[43:55]
|
2017-08-10 13:49:14 +08:00
|
|
|
layout := "Jan 2 15:04"
|
2017-08-10 13:12:50 +08:00
|
|
|
_, err := time.Parse(layout, got)
|
2017-08-13 20:00:08 +08:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
layout = "Jan 2 2006"
|
|
|
|
_, err = time.Parse(layout, got)
|
|
|
|
}
|
2017-08-10 13:12:50 +08:00
|
|
|
if err != nil {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): mod time field mismatch, expected date layout %s, got: %#v, err: %#v", path, layout, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// spacer
|
|
|
|
got = result[55:56]
|
|
|
|
if ok, err := regexp.MatchString("^\\s$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): spacer 6 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// filename
|
|
|
|
got = result[56:]
|
|
|
|
if ok, err := regexp.MatchString("^"+path+"$", got); !ok {
|
2017-08-13 20:00:08 +08:00
|
|
|
t.Errorf("runLs(%#v, *FileInfo): name field mismatch, expected examples, got: %#v, err: %#v", path, got, err)
|
2017-08-10 13:12:50 +08:00
|
|
|
}
|
2017-08-10 13:49:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func clientServerPair(t *testing.T) (*Client, *Server) {
|
|
|
|
cr, sw := io.Pipe()
|
|
|
|
sr, cw := io.Pipe()
|
2020-03-18 16:36:07 +08:00
|
|
|
var options []ServerOption
|
|
|
|
if *testAllocator {
|
|
|
|
options = append(options, WithAllocator())
|
|
|
|
}
|
2017-08-10 13:49:14 +08:00
|
|
|
server, err := NewServer(struct {
|
|
|
|
io.Reader
|
|
|
|
io.WriteCloser
|
2020-03-18 16:36:07 +08:00
|
|
|
}{sr, sw}, options...)
|
2017-08-10 13:49:14 +08:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
go server.Serve()
|
|
|
|
client, err := NewClientPipe(cr, cw)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("%+v\n", err)
|
|
|
|
}
|
|
|
|
return client, server
|
|
|
|
}
|
|
|
|
|
|
|
|
type sshFxpTestBadExtendedPacket struct {
|
|
|
|
ID uint32
|
|
|
|
Extension string
|
|
|
|
Data string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p sshFxpTestBadExtendedPacket) id() uint32 { return p.ID }
|
|
|
|
|
|
|
|
func (p sshFxpTestBadExtendedPacket) MarshalBinary() ([]byte, error) {
|
|
|
|
l := 1 + 4 + 4 + // type(byte) + uint32 + uint32
|
|
|
|
len(p.Extension) +
|
|
|
|
len(p.Data)
|
|
|
|
|
|
|
|
b := make([]byte, 0, l)
|
2019-08-30 23:04:37 +08:00
|
|
|
b = append(b, sshFxpExtended)
|
2017-08-10 13:49:14 +08:00
|
|
|
b = marshalUint32(b, p.ID)
|
|
|
|
b = marshalString(b, p.Extension)
|
|
|
|
b = marshalString(b, p.Data)
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
2020-03-15 02:42:19 +08:00
|
|
|
func checkServerAllocator(t *testing.T, server *Server) {
|
|
|
|
if server.pktMgr.alloc == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
checkAllocatorBeforeServerClose(t, server.pktMgr.alloc)
|
|
|
|
server.Close()
|
|
|
|
checkAllocatorAfterServerClose(t, server.pktMgr.alloc)
|
|
|
|
}
|
|
|
|
|
2017-08-10 13:49:14 +08:00
|
|
|
// test that errors are sent back when we request an invalid extended packet operation
|
2018-03-19 22:32:22 +08:00
|
|
|
// this validates the following rfc draft is followed https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00
|
2017-08-10 13:49:14 +08:00
|
|
|
func TestInvalidExtendedPacket(t *testing.T) {
|
|
|
|
client, server := clientServerPair(t)
|
|
|
|
defer client.Close()
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
badPacket := sshFxpTestBadExtendedPacket{client.nextID(), "thisDoesn'tExist", "foobar"}
|
2018-03-19 22:32:22 +08:00
|
|
|
typ, data, err := client.clientConn.sendPacket(badPacket)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error from sendPacket: %s", err)
|
2017-08-10 13:49:14 +08:00
|
|
|
}
|
2019-08-30 23:04:37 +08:00
|
|
|
if typ != sshFxpStatus {
|
2018-03-19 22:32:22 +08:00
|
|
|
t.Fatalf("received non-FPX_STATUS packet: %v", typ)
|
2017-08-10 13:49:14 +08:00
|
|
|
}
|
|
|
|
|
2018-03-19 22:32:22 +08:00
|
|
|
err = unmarshalStatus(badPacket.id(), data)
|
|
|
|
statusErr, ok := err.(*StatusError)
|
|
|
|
if !ok {
|
|
|
|
t.Fatal("failed to convert error from unmarshalStatus to *StatusError")
|
|
|
|
}
|
2019-08-30 23:04:37 +08:00
|
|
|
if statusErr.Code != sshFxOPUnsupported {
|
|
|
|
t.Errorf("statusErr.Code => %d, wanted %d", statusErr.Code, sshFxOPUnsupported)
|
2018-03-19 22:32:22 +08:00
|
|
|
}
|
2020-03-15 02:42:19 +08:00
|
|
|
checkServerAllocator(t, server)
|
2017-08-10 13:49:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// test that server handles concurrent requests correctly
|
|
|
|
func TestConcurrentRequests(t *testing.T) {
|
2019-01-19 07:59:30 +08:00
|
|
|
skipIfWindows(t)
|
2020-09-11 00:11:47 +08:00
|
|
|
filename := "/etc/passwd"
|
|
|
|
if runtime.GOOS == "plan9" {
|
|
|
|
filename = "/lib/ndb/local"
|
|
|
|
}
|
2017-08-10 13:49:14 +08:00
|
|
|
client, server := clientServerPair(t)
|
|
|
|
defer client.Close()
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
concurrency := 2
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(concurrency)
|
|
|
|
|
|
|
|
for i := 0; i < concurrency; i++ {
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
for j := 0; j < 1024; j++ {
|
2020-09-11 00:11:47 +08:00
|
|
|
f, err := client.Open(filename)
|
2017-08-10 13:49:14 +08:00
|
|
|
if err != nil {
|
|
|
|
t.Errorf("failed to open file: %v", err)
|
2020-09-11 00:11:47 +08:00
|
|
|
continue
|
2017-08-10 13:49:14 +08:00
|
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
t.Errorf("failed t close file: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
wg.Wait()
|
2020-03-15 02:42:19 +08:00
|
|
|
checkServerAllocator(t, server)
|
2017-08-10 13:49:14 +08:00
|
|
|
}
|
2018-01-27 09:26:44 +08:00
|
|
|
|
|
|
|
// Test error conversion
|
|
|
|
func TestStatusFromError(t *testing.T) {
|
|
|
|
type test struct {
|
|
|
|
err error
|
|
|
|
pkt sshFxpStatusPacket
|
|
|
|
}
|
|
|
|
tpkt := func(id, code uint32) sshFxpStatusPacket {
|
|
|
|
return sshFxpStatusPacket{
|
|
|
|
ID: id,
|
|
|
|
StatusError: StatusError{Code: code},
|
|
|
|
}
|
|
|
|
}
|
2019-08-30 23:04:37 +08:00
|
|
|
testCases := []test{
|
2020-07-25 16:17:41 +08:00
|
|
|
{syscall.ENOENT, tpkt(1, sshFxNoSuchFile)},
|
|
|
|
{&os.PathError{Err: syscall.ENOENT},
|
2019-08-30 23:04:37 +08:00
|
|
|
tpkt(2, sshFxNoSuchFile)},
|
2020-07-25 16:17:41 +08:00
|
|
|
{&os.PathError{Err: errors.New("foo")}, tpkt(3, sshFxFailure)},
|
|
|
|
{ErrSSHFxEOF, tpkt(4, sshFxEOF)},
|
|
|
|
{ErrSSHFxOpUnsupported, tpkt(5, sshFxOPUnsupported)},
|
|
|
|
{io.EOF, tpkt(6, sshFxEOF)},
|
|
|
|
{os.ErrNotExist, tpkt(7, sshFxNoSuchFile)},
|
2018-01-27 09:26:44 +08:00
|
|
|
}
|
2019-08-30 23:04:37 +08:00
|
|
|
for _, tc := range testCases {
|
2018-01-27 09:26:44 +08:00
|
|
|
tc.pkt.StatusError.msg = tc.err.Error()
|
|
|
|
assert.Equal(t, tc.pkt, statusFromError(tc.pkt, tc.err))
|
|
|
|
}
|
|
|
|
}
|
2018-08-21 03:39:58 +08:00
|
|
|
|
|
|
|
// This was written to test a race b/w open immediately followed by a stat.
|
|
|
|
// Previous to this the Open would trigger the use of a worker pool, then the
|
|
|
|
// stat packet would come in an hit the pool and return faster than the open
|
|
|
|
// (returning a file-not-found error).
|
|
|
|
// The below by itself wouldn't trigger the race however, I needed to add a
|
|
|
|
// small sleep in the openpacket code to trigger the issue. I wanted to add a
|
|
|
|
// way to inject that in the code but right now there is no good place for it.
|
|
|
|
// I'm thinking after I convert the server into a request-server backend I
|
|
|
|
// might be able to do something with the runWorker method passed into the
|
|
|
|
// packet manager. But with the 2 implementations fo the server it just doesn't
|
|
|
|
// fit well right now.
|
|
|
|
func TestOpenStatRace(t *testing.T) {
|
|
|
|
client, server := clientServerPair(t)
|
|
|
|
defer client.Close()
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
// openpacket finishes to fast to trigger race in tests
|
|
|
|
// need to add a small sleep on server to openpackets somehow
|
|
|
|
tmppath := path.Join(os.TempDir(), "stat_race")
|
|
|
|
pflags := flags(os.O_RDWR | os.O_CREATE | os.O_TRUNC)
|
|
|
|
ch := make(chan result, 3)
|
|
|
|
id1 := client.nextID()
|
|
|
|
client.dispatchRequest(ch, sshFxpOpenPacket{
|
|
|
|
ID: id1,
|
|
|
|
Path: tmppath,
|
|
|
|
Pflags: pflags,
|
|
|
|
})
|
|
|
|
id2 := client.nextID()
|
|
|
|
client.dispatchRequest(ch, sshFxpLstatPacket{
|
|
|
|
ID: id2,
|
|
|
|
Path: tmppath,
|
|
|
|
})
|
|
|
|
testreply := func(id uint32, ch chan result) {
|
|
|
|
r := <-ch
|
|
|
|
switch r.typ {
|
2019-08-30 23:04:37 +08:00
|
|
|
case sshFxpAttrs, sshFxpHandle: // ignore
|
|
|
|
case sshFxpStatus:
|
2018-08-21 03:39:58 +08:00
|
|
|
err := normaliseError(unmarshalStatus(id, r.data))
|
|
|
|
assert.NoError(t, err, "race hit, stat before open")
|
|
|
|
default:
|
|
|
|
assert.Fail(t, "Unexpected type")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
testreply(id1, ch)
|
|
|
|
testreply(id2, ch)
|
|
|
|
os.Remove(tmppath)
|
2020-03-15 02:42:19 +08:00
|
|
|
checkServerAllocator(t, server)
|
2018-08-21 03:39:58 +08:00
|
|
|
}
|
2020-09-14 12:38:04 +08:00
|
|
|
|
|
|
|
// Ensure that proper error codes are returned for non existent files, such
|
|
|
|
// that they are mapped back to a 'not exists' error on the client side.
|
|
|
|
func TestStatNonExistent(t *testing.T) {
|
|
|
|
client, server := clientServerPair(t)
|
|
|
|
defer client.Close()
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
for _, file := range []string{"/doesnotexist", "/doesnotexist/a/b"} {
|
|
|
|
_, err := client.Stat(file)
|
2020-09-14 22:58:12 +08:00
|
|
|
if !os.IsNotExist(err) {
|
2020-09-14 12:38:04 +08:00
|
|
|
t.Errorf("expected 'does not exist' err for file %q. got: %v", file, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-30 16:36:20 +08:00
|
|
|
|
|
|
|
func TestServerWithBrokenClient(t *testing.T) {
|
|
|
|
validInit := sp(sshFxInitPacket{Version: 3})
|
|
|
|
brokenOpen := sp(sshFxpOpenPacket{Path: "foo"})
|
|
|
|
brokenOpen = brokenOpen[:len(brokenOpen)-2]
|
|
|
|
|
|
|
|
for _, clientInput := range [][]byte{
|
|
|
|
// Packet length zero (never valid). This used to crash the server.
|
|
|
|
{0, 0, 0, 0},
|
|
|
|
append(validInit, 0, 0, 0, 0),
|
|
|
|
|
|
|
|
// Client hangs up mid-packet.
|
|
|
|
append(validInit, brokenOpen...),
|
|
|
|
} {
|
|
|
|
srv, err := NewServer(struct {
|
|
|
|
io.Reader
|
|
|
|
io.WriteCloser
|
|
|
|
}{
|
|
|
|
bytes.NewReader(clientInput),
|
|
|
|
&sink{},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = srv.Serve()
|
|
|
|
assert.Error(t, err)
|
|
|
|
srv.Close()
|
|
|
|
}
|
|
|
|
}
|