From 0fb7b11fb414fe3b94d96d2b0d980cb1f0cfaf17 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Tue, 13 Dec 2016 21:56:31 -0500 Subject: [PATCH] Add a test for the POP3 server. Fixes issues with trying to operate on deleted messages. --- pop3/conn.go | 17 ++- pop3/conn_test.go | 264 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 pop3/conn_test.go diff --git a/pop3/conn.go b/pop3/conn.go index 91a2543..94173be 100644 --- a/pop3/conn.go +++ b/pop3/conn.go @@ -17,9 +17,10 @@ const ( ) const ( - errStateAuth = "not in AUTHORIZATION" - errStateTxn = "not in TRANSACTION" - errSyntax = "syntax error" + errStateAuth = "not in AUTHORIZATION" + errStateTxn = "not in TRANSACTION" + errSyntax = "syntax error" + errDeletedMsg = "no such message - deleted" ) type connection struct { @@ -201,6 +202,11 @@ func (conn *connection) doRETR() { return } + if msg.Deleted() { + conn.err(errDeletedMsg) + return + } + rc, err := conn.mb.Retrieve(msg) if err != nil { conn.err(err.Error()) @@ -223,6 +229,11 @@ func (conn *connection) doDELE() { return } + if msg.Deleted() { + conn.err(errDeletedMsg) + return + } + if err := conn.mb.Delete(msg); err != nil { conn.err(err.Error()) } else { diff --git a/pop3/conn_test.go b/pop3/conn_test.go new file mode 100644 index 0000000..06c68bf --- /dev/null +++ b/pop3/conn_test.go @@ -0,0 +1,264 @@ +package pop3 + +import ( + "fmt" + "io" + "net" + "net/textproto" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func _fl(depth int) string { + _, file, line, _ := runtime.Caller(depth + 1) + return fmt.Sprintf("[%s:%d]", filepath.Base(file), line) +} + +func ok(t testing.TB, err error) { + if err != nil { + t.Errorf("%s unexpected error: %v", _fl(1), err) + } +} + +func responseOK(t testing.TB, conn *textproto.Conn) string { + line, err := conn.ReadLine() + if err != nil { + t.Errorf("%s responseOK: %v", _fl(1), err) + } + if !strings.HasPrefix(line, "+OK") { + t.Errorf("%s expected +OK, got %q", _fl(1), line) + } + return line +} + +func responseERR(t testing.TB, conn *textproto.Conn) string { + line, err := conn.ReadLine() + if err != nil { + t.Errorf("%s responseERR: %v", _fl(1), err) + } + if !strings.HasPrefix(line, "-ERR") { + t.Errorf("%s expected -ERR, got %q", _fl(1), line) + } + return line +} + +func runServer(t *testing.T, po PostOffice) net.Listener { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatal(err) + return nil + } + + go func() { + for { + conn, err := l.Accept() + if err != nil { + return + } + go AcceptConnection(conn, po) + } + }() + return l +} + +type testServer struct { + user, pass string + mb testMailbox +} + +func (s *testServer) Name() string { + return "Test-Server" +} + +func (s *testServer) OpenMailbox(user, pass string) (Mailbox, error) { + if s.user == user && s.pass == pass { + return &s.mb, nil + } + return nil, fmt.Errorf("bad username/pass") +} + +type testMailbox struct { + msgs map[int]*testMessage +} + +func (mb *testMailbox) ListMessages() ([]Message, error) { + msgs := make([]Message, 0, len(mb.msgs)) + for i, _ := range mb.msgs { + msgs = append(msgs, mb.msgs[i]) + } + return msgs, nil +} + +func (mb *testMailbox) GetMessage(id int) Message { + return mb.msgs[id] +} + +func (mb *testMailbox) Retrieve(msg Message) (io.ReadCloser, error) { + return nil, nil +} + +func (mb *testMailbox) Delete(msg Message) error { + msg.(*testMessage).deleted = true + return nil +} + +func (mb *testMailbox) Close() error { + return nil +} + +func (mb *testMailbox) Reset() { + for _, msg := range mb.msgs { + msg.deleted = false + } +} + +type testMessage struct { + id int + size int + deleted bool +} + +func (m *testMessage) ID() int { + return m.id +} +func (m *testMessage) Size() int { + return m.size +} +func (m *testMessage) Deleted() bool { + return m.deleted +} + +func newTestServer() *testServer { + return &testServer{ + user: "u", + pass: "p", + mb: testMailbox{ + msgs: make(map[int]*testMessage), + }, + } +} + +// RFC 1939 § 10 +func TestExampleSession(t *testing.T) { + s := newTestServer() + l := runServer(t, s) + defer l.Close() + + s.mb.msgs[1] = &testMessage{1, 120, false} + s.mb.msgs[2] = &testMessage{2, 200, false} + + conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String()) + ok(t, err) + + line := responseOK(t, conn) + if !strings.Contains(line, s.Name()) { + t.Errorf("POP greeting did not include server name, got %q", line) + } + + ok(t, conn.PrintfLine("USER u")) + responseOK(t, conn) + + ok(t, conn.PrintfLine("PASS p")) + responseOK(t, conn) + + ok(t, conn.PrintfLine("STAT")) + line = responseOK(t, conn) + expected := "+OK 2 320" + if line != expected { + t.Errorf("STAT expected %q, got %q", expected, line) + } + + ok(t, conn.PrintfLine("LIST")) + responseOK(t, conn) + lines, err := conn.ReadDotLines() + ok(t, err) + if len(lines) != 2 { + t.Errorf("LIST expected 2 lines, got %d", len(lines)) + } + expected = "1 120" + if lines[0] != expected { + t.Errorf("LIST line 0 expected %q, got %q", expected, lines[0]) + } + expected = "2 200" + if lines[1] != expected { + t.Errorf("LIST line 1 expected %q, got %q", expected, lines[1]) + } + + ok(t, conn.PrintfLine("QUIT")) + responseOK(t, conn) +} + +type requestResponse struct { + command string + expecter func(testing.TB, *textproto.Conn) string +} + +func expectOKResponse(predicate func(string) bool) func(testing.TB, *textproto.Conn) string { + return func(t testing.TB, conn *textproto.Conn) string { + line := responseOK(t, conn) + if !predicate(line) { + t.Errorf("%s Predicate failed, got %q", _fl(1), line) + } + return line + } +} + +func clientServerTest(t *testing.T, s *testServer, sequence []requestResponse) { + l := runServer(t, s) + defer l.Close() + + conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String()) + ok(t, err) + + responseOK(t, conn) + + for _, pair := range sequence { + ok(t, conn.PrintfLine(pair.command)) + pair.expecter(t, conn) + if t.Failed() { + t.Logf("command %q", pair.command) + } + } +} + +func TestAuthStates(t *testing.T) { + clientServerTest(t, newTestServer(), []requestResponse{ + {"STAT", responseERR}, + {"NOOP", responseOK}, + {"USER bad", responseOK}, + {"PASS bad", responseERR}, + {"LIST", responseERR}, + {"USER u", responseOK}, + {"PASS bad", responseERR}, + {"STAT", responseERR}, + {"PASS p", responseOK}, + {"QUIT", responseOK}, + }) +} + +func TestDeleted(t *testing.T) { + s := newTestServer() + s.mb.msgs[1] = &testMessage{1, 999, false} + s.mb.msgs[2] = &testMessage{2, 10, false} + + clientServerTest(t, s, []requestResponse{ + {"USER u", responseOK}, + {"PASS p", responseOK}, + {"STAT", expectOKResponse(func(s string) bool { + return s == "+OK 2 1009" + })}, + {"DELE 1", responseOK}, + {"RETR 1", responseERR}, + {"DELE 1", responseERR}, + {"STAT", expectOKResponse(func(s string) bool { + return s == "+OK 1 10" + })}, + {"RSET", responseOK}, + {"STAT", expectOKResponse(func(s string) bool { + return s == "+OK 2 1009" + })}, + {"QUIT", responseOK}, + }) +} -- 2.22.5