Add a test for the POP3 server.
[mailpopbox.git] / pop3 / conn_test.go
1 package pop3
2
3 import (
4 "fmt"
5 "io"
6 "net"
7 "net/textproto"
8 "path/filepath"
9 "runtime"
10 "strings"
11 "testing"
12 )
13
14 func _fl(depth int) string {
15 _, file, line, _ := runtime.Caller(depth + 1)
16 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
17 }
18
19 func ok(t testing.TB, err error) {
20 if err != nil {
21 t.Errorf("%s unexpected error: %v", _fl(1), err)
22 }
23 }
24
25 func responseOK(t testing.TB, conn *textproto.Conn) string {
26 line, err := conn.ReadLine()
27 if err != nil {
28 t.Errorf("%s responseOK: %v", _fl(1), err)
29 }
30 if !strings.HasPrefix(line, "+OK") {
31 t.Errorf("%s expected +OK, got %q", _fl(1), line)
32 }
33 return line
34 }
35
36 func responseERR(t testing.TB, conn *textproto.Conn) string {
37 line, err := conn.ReadLine()
38 if err != nil {
39 t.Errorf("%s responseERR: %v", _fl(1), err)
40 }
41 if !strings.HasPrefix(line, "-ERR") {
42 t.Errorf("%s expected -ERR, got %q", _fl(1), line)
43 }
44 return line
45 }
46
47 func runServer(t *testing.T, po PostOffice) net.Listener {
48 l, err := net.Listen("tcp", "localhost:0")
49 if err != nil {
50 t.Fatal(err)
51 return nil
52 }
53
54 go func() {
55 for {
56 conn, err := l.Accept()
57 if err != nil {
58 return
59 }
60 go AcceptConnection(conn, po)
61 }
62 }()
63 return l
64 }
65
66 type testServer struct {
67 user, pass string
68 mb testMailbox
69 }
70
71 func (s *testServer) Name() string {
72 return "Test-Server"
73 }
74
75 func (s *testServer) OpenMailbox(user, pass string) (Mailbox, error) {
76 if s.user == user && s.pass == pass {
77 return &s.mb, nil
78 }
79 return nil, fmt.Errorf("bad username/pass")
80 }
81
82 type testMailbox struct {
83 msgs map[int]*testMessage
84 }
85
86 func (mb *testMailbox) ListMessages() ([]Message, error) {
87 msgs := make([]Message, 0, len(mb.msgs))
88 for i, _ := range mb.msgs {
89 msgs = append(msgs, mb.msgs[i])
90 }
91 return msgs, nil
92 }
93
94 func (mb *testMailbox) GetMessage(id int) Message {
95 return mb.msgs[id]
96 }
97
98 func (mb *testMailbox) Retrieve(msg Message) (io.ReadCloser, error) {
99 return nil, nil
100 }
101
102 func (mb *testMailbox) Delete(msg Message) error {
103 msg.(*testMessage).deleted = true
104 return nil
105 }
106
107 func (mb *testMailbox) Close() error {
108 return nil
109 }
110
111 func (mb *testMailbox) Reset() {
112 for _, msg := range mb.msgs {
113 msg.deleted = false
114 }
115 }
116
117 type testMessage struct {
118 id int
119 size int
120 deleted bool
121 }
122
123 func (m *testMessage) ID() int {
124 return m.id
125 }
126 func (m *testMessage) Size() int {
127 return m.size
128 }
129 func (m *testMessage) Deleted() bool {
130 return m.deleted
131 }
132
133 func newTestServer() *testServer {
134 return &testServer{
135 user: "u",
136 pass: "p",
137 mb: testMailbox{
138 msgs: make(map[int]*testMessage),
139 },
140 }
141 }
142
143 // RFC 1939 ยง 10
144 func TestExampleSession(t *testing.T) {
145 s := newTestServer()
146 l := runServer(t, s)
147 defer l.Close()
148
149 s.mb.msgs[1] = &testMessage{1, 120, false}
150 s.mb.msgs[2] = &testMessage{2, 200, false}
151
152 conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String())
153 ok(t, err)
154
155 line := responseOK(t, conn)
156 if !strings.Contains(line, s.Name()) {
157 t.Errorf("POP greeting did not include server name, got %q", line)
158 }
159
160 ok(t, conn.PrintfLine("USER u"))
161 responseOK(t, conn)
162
163 ok(t, conn.PrintfLine("PASS p"))
164 responseOK(t, conn)
165
166 ok(t, conn.PrintfLine("STAT"))
167 line = responseOK(t, conn)
168 expected := "+OK 2 320"
169 if line != expected {
170 t.Errorf("STAT expected %q, got %q", expected, line)
171 }
172
173 ok(t, conn.PrintfLine("LIST"))
174 responseOK(t, conn)
175 lines, err := conn.ReadDotLines()
176 ok(t, err)
177 if len(lines) != 2 {
178 t.Errorf("LIST expected 2 lines, got %d", len(lines))
179 }
180 expected = "1 120"
181 if lines[0] != expected {
182 t.Errorf("LIST line 0 expected %q, got %q", expected, lines[0])
183 }
184 expected = "2 200"
185 if lines[1] != expected {
186 t.Errorf("LIST line 1 expected %q, got %q", expected, lines[1])
187 }
188
189 ok(t, conn.PrintfLine("QUIT"))
190 responseOK(t, conn)
191 }
192
193 type requestResponse struct {
194 command string
195 expecter func(testing.TB, *textproto.Conn) string
196 }
197
198 func expectOKResponse(predicate func(string) bool) func(testing.TB, *textproto.Conn) string {
199 return func(t testing.TB, conn *textproto.Conn) string {
200 line := responseOK(t, conn)
201 if !predicate(line) {
202 t.Errorf("%s Predicate failed, got %q", _fl(1), line)
203 }
204 return line
205 }
206 }
207
208 func clientServerTest(t *testing.T, s *testServer, sequence []requestResponse) {
209 l := runServer(t, s)
210 defer l.Close()
211
212 conn, err := textproto.Dial(l.Addr().Network(), l.Addr().String())
213 ok(t, err)
214
215 responseOK(t, conn)
216
217 for _, pair := range sequence {
218 ok(t, conn.PrintfLine(pair.command))
219 pair.expecter(t, conn)
220 if t.Failed() {
221 t.Logf("command %q", pair.command)
222 }
223 }
224 }
225
226 func TestAuthStates(t *testing.T) {
227 clientServerTest(t, newTestServer(), []requestResponse{
228 {"STAT", responseERR},
229 {"NOOP", responseOK},
230 {"USER bad", responseOK},
231 {"PASS bad", responseERR},
232 {"LIST", responseERR},
233 {"USER u", responseOK},
234 {"PASS bad", responseERR},
235 {"STAT", responseERR},
236 {"PASS p", responseOK},
237 {"QUIT", responseOK},
238 })
239 }
240
241 func TestDeleted(t *testing.T) {
242 s := newTestServer()
243 s.mb.msgs[1] = &testMessage{1, 999, false}
244 s.mb.msgs[2] = &testMessage{2, 10, false}
245
246 clientServerTest(t, s, []requestResponse{
247 {"USER u", responseOK},
248 {"PASS p", responseOK},
249 {"STAT", expectOKResponse(func(s string) bool {
250 return s == "+OK 2 1009"
251 })},
252 {"DELE 1", responseOK},
253 {"RETR 1", responseERR},
254 {"DELE 1", responseERR},
255 {"STAT", expectOKResponse(func(s string) bool {
256 return s == "+OK 1 10"
257 })},
258 {"RSET", responseOK},
259 {"STAT", expectOKResponse(func(s string) bool {
260 return s == "+OK 2 1009"
261 })},
262 {"QUIT", responseOK},
263 })
264 }