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