Add a test for the POP3 server.
authorRobert Sesek <rsesek@bluestatic.org>
Wed, 14 Dec 2016 02:56:31 +0000 (21:56 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Wed, 14 Dec 2016 03:04:21 +0000 (22:04 -0500)
Fixes issues with trying to operate on deleted messages.

pop3/conn.go
pop3/conn_test.go [new file with mode: 0644]

index 91a25437fe52ba4813647926f3f65d08ba36b358..94173be7bfe02570f55b2f821906228a175e54a0 100644 (file)
@@ -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 (file)
index 0000000..06c68bf
--- /dev/null
@@ -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},
+       })
+}