• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.email.mail.transport;
18 
19 import com.android.email.mail.Transport;
20 
21 import android.util.Log;
22 
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.net.InetAddress;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.regex.Pattern;
30 
31 import junit.framework.Assert;
32 
33 /**
34  * This is a mock Transport that is used to test protocols that use MailTransport.
35  */
36 public class MockTransport implements Transport {
37 
38     // All flags defining debug or development code settings must be FALSE
39     // when code is checked in or released.
40     private static boolean DEBUG_LOG_STREAMS = true;
41 
42     private static String LOG_TAG = "MockTransport";
43 
44     private static final String SPECIAL_RESPONSE_IOEXCEPTION = "!!!IOEXCEPTION!!!";
45 
46     private boolean mTlsStarted = false;
47 
48     private boolean mOpen;
49     private boolean mInputOpen;
50     private int mConnectionSecurity;
51     private boolean mTrustCertificates;
52     private String mHost;
53     private InetAddress mLocalAddress;
54 
55     private ArrayList<String> mQueuedInput = new ArrayList<String>();
56 
57     private static class Transaction {
58         public static final int ACTION_INJECT_TEXT = 0;
59         public static final int ACTION_CLIENT_CLOSE = 1;
60         public static final int ACTION_IO_EXCEPTION = 2;
61         public static final int ACTION_START_TLS = 3;
62 
63         int mAction;
64         String mPattern;
65         String[] mResponses;
66 
Transaction(String pattern, String[] responses)67         Transaction(String pattern, String[] responses) {
68             mAction = ACTION_INJECT_TEXT;
69             mPattern = pattern;
70             mResponses = responses;
71         }
72 
Transaction(int otherType)73         Transaction(int otherType) {
74             mAction = otherType;
75             mPattern = null;
76             mResponses = null;
77         }
78 
79         @Override
toString()80         public String toString() {
81             switch (mAction) {
82                 case ACTION_INJECT_TEXT:
83                     return mPattern + ": " + Arrays.toString(mResponses);
84                 case ACTION_CLIENT_CLOSE:
85                     return "Expect the client to close";
86                 case ACTION_IO_EXCEPTION:
87                     return "Expect IOException";
88                 case ACTION_START_TLS:
89                     return "Expect StartTls";
90                 default:
91                     return "(Hmm.  Unknown action.)";
92             }
93         }
94     }
95 
96     private ArrayList<Transaction> mPairs = new ArrayList<Transaction>();
97 
98     /**
99      * Give the mock a pattern to wait for.  No response will be sent.
100      * @param pattern Java RegEx to wait for
101      */
expect(String pattern)102     public void expect(String pattern) {
103         expect(pattern, (String[])null);
104     }
105 
106     /**
107      * Give the mock a pattern to wait for and a response to send back.
108      * @param pattern Java RegEx to wait for
109      * @param response String to reply with, or null to acccept string but not respond to it
110      */
expect(String pattern, String response)111     public void expect(String pattern, String response) {
112         expect(pattern, (response == null) ? null : new String[] { response });
113     }
114 
115     /**
116      * Give the mock a pattern to wait for and a multi-line response to send back.
117      * @param pattern Java RegEx to wait for
118      * @param responses Strings to reply with
119      */
expect(String pattern, String[] responses)120     public void expect(String pattern, String[] responses) {
121         Transaction pair = new Transaction(pattern, responses);
122         mPairs.add(pair);
123     }
124 
125     /**
126      * Same as {@link #expect(String, String[])}, but the first arg is taken literally, rather than
127      * as a regexp.
128      */
expectLiterally(String literal, String[] responses)129     public void expectLiterally(String literal, String[] responses) {
130         expect("^" + Pattern.quote(literal) + "$", responses);
131     }
132 
133     /**
134      * Tell the Mock Transport that we expect it to be closed.  This will preserve
135      * the remaining entries in the expect() stream and allow us to "ride over" the close (which
136      * would normally reset everything).
137      */
expectClose()138     public void expectClose() {
139         mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE));
140     }
141 
expectIOException()142     public void expectIOException() {
143         mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
144     }
145 
expectStartTls()146     public void expectStartTls() {
147         mPairs.add(new Transaction(Transaction.ACTION_START_TLS));
148     }
149 
sendResponse(Transaction pair)150     private void sendResponse(Transaction pair) {
151         switch (pair.mAction) {
152             case Transaction.ACTION_INJECT_TEXT:
153                 for (String s : pair.mResponses) {
154                     mQueuedInput.add(s);
155                 }
156                 break;
157             case Transaction.ACTION_IO_EXCEPTION:
158                 mQueuedInput.add(SPECIAL_RESPONSE_IOEXCEPTION);
159                 break;
160             default:
161                 Assert.fail("Invalid action for sendResponse: " + pair.mAction);
162         }
163     }
164 
165     @Override
canTrySslSecurity()166     public boolean canTrySslSecurity() {
167         return (mConnectionSecurity == CONNECTION_SECURITY_SSL);
168     }
169 
170     @Override
canTryTlsSecurity()171     public boolean canTryTlsSecurity() {
172         return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS);
173     }
174 
175     @Override
canTrustAllCertificates()176     public boolean canTrustAllCertificates() {
177         return mTrustCertificates;
178     }
179 
180     /**
181      * Check that TLS was started
182      */
isTlsStarted()183     public boolean isTlsStarted() {
184         return mTlsStarted;
185     }
186 
187     /**
188      * This simulates a condition where the server has closed its side, causing
189      * reads to fail.
190      */
closeInputStream()191     public void closeInputStream() {
192         mInputOpen = false;
193     }
194 
195     @Override
close()196     public void close() {
197         mOpen = false;
198         mInputOpen = false;
199         // unless it was expected as part of a test, reset the stream
200         if (mPairs.size() > 0) {
201             Transaction expect = mPairs.remove(0);
202             if (expect.mAction == Transaction.ACTION_CLIENT_CLOSE) {
203                 return;
204             }
205         }
206         mQueuedInput.clear();
207         mPairs.clear();
208     }
209 
210     @Override
setHost(String host)211     public void setHost(String host) {
212         mHost = host;
213     }
214 
215     @Override
getHost()216     public String getHost() {
217         return mHost;
218     }
219 
setMockLocalAddress(InetAddress address)220     public void setMockLocalAddress(InetAddress address) {
221         mLocalAddress = address;
222     }
223 
224     @Override
getInputStream()225     public InputStream getInputStream() {
226         SmtpSenderUnitTests.assertTrue(mOpen);
227         return new MockInputStream();
228     }
229 
230     /**
231      * This normally serves as a pseudo-clone, for use by Imap.  For the purposes of unit testing,
232      * until we need something more complex, we'll just return the actual MockTransport.  Then we
233      * don't have to worry about dealing with test metadata like the expects list or socket state.
234      */
235     @Override
clone()236     public Transport clone() {
237          return this;
238     }
239 
240     @Override
getOutputStream()241     public OutputStream getOutputStream() {
242         Assert.assertTrue(mOpen);
243         return new MockOutputStream();
244     }
245 
246     @Override
setPort(int port)247     public void setPort(int port) {
248         SmtpSenderUnitTests.fail("setPort() not implemented");
249     }
250 
251     @Override
getPort()252     public int getPort() {
253         SmtpSenderUnitTests.fail("getPort() not implemented");
254         return 0;
255     }
256 
257     @Override
getSecurity()258     public int getSecurity() {
259         return mConnectionSecurity;
260     }
261 
262     @Override
isOpen()263     public boolean isOpen() {
264         return mOpen;
265     }
266 
267     @Override
open()268     public void open() /* throws MessagingException, CertificateValidationException */ {
269         mOpen = true;
270         mInputOpen = true;
271     }
272 
273     /**
274      * This returns one string (if available) to the caller.  Usually this simply pulls strings
275      * from the mQueuedInput list, but if the list is empty, we also peek the expect list.  This
276      * supports banners, multi-line responses, and any other cases where we respond without
277      * a specific expect pattern.
278      *
279      * If no response text is available, we assert (failing our test) as an underflow.
280      *
281      * Logs the read text if DEBUG_LOG_STREAMS is true.
282      */
283     @Override
readLine()284     public String readLine() throws IOException {
285         SmtpSenderUnitTests.assertTrue(mOpen);
286         if (!mInputOpen) {
287             throw new IOException("Reading from MockTransport with closed input");
288         }
289         // if there's nothing to read, see if we can find a null-pattern response
290         if ((mQueuedInput.size() == 0) && (mPairs.size() > 0)) {
291             Transaction pair = mPairs.get(0);
292             if (pair.mPattern == null) {
293                 mPairs.remove(0);
294                 sendResponse(pair);
295             }
296         }
297         if (mQueuedInput.size() == 0) {
298             // MailTransport returns "" at EOS.
299             Log.w(LOG_TAG, "Underflow reading from MockTransport");
300             return "";
301         }
302         String line = mQueuedInput.remove(0);
303         if (DEBUG_LOG_STREAMS) {
304             Log.d(LOG_TAG, "<<< " + line);
305         }
306         if (SPECIAL_RESPONSE_IOEXCEPTION.equals(line)) {
307             throw new IOException("Expected IOException.");
308         }
309         return line;
310     }
311 
312     @Override
reopenTls()313     public void reopenTls() /* throws MessagingException */ {
314         SmtpSenderUnitTests.assertTrue(mOpen);
315         Transaction expect = mPairs.remove(0);
316         SmtpSenderUnitTests.assertTrue(expect.mAction == Transaction.ACTION_START_TLS);
317         mTlsStarted = true;
318     }
319 
320     @Override
setSecurity(int connectionSecurity, boolean trustAllCertificates)321     public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
322         mConnectionSecurity = connectionSecurity;
323         mTrustCertificates = trustAllCertificates;
324     }
325 
326     @Override
setSoTimeout(int timeoutMilliseconds)327     public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ {
328     }
329 
330     /**
331      * Accepts a single string (command or text) that was written by the code under test.
332      * Because we are essentially mocking a server, we check to see if this string was expected.
333      * If the string was expected, we push the corresponding responses into the mQueuedInput
334      * list, for subsequent calls to readLine().  If the string does not match, we assert
335      * the mismatch.  If no string was expected, we assert it as an overflow.
336      *
337      * Logs the written text if DEBUG_LOG_STREAMS is true.
338      */
339     @Override
writeLine(String s, String sensitiveReplacement)340     public void writeLine(String s, String sensitiveReplacement) throws IOException {
341         if (DEBUG_LOG_STREAMS) {
342             Log.d(LOG_TAG, ">>> " + s);
343         }
344         SmtpSenderUnitTests.assertTrue(mOpen);
345         SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s,
346                 0 != mPairs.size());
347         Transaction pair = mPairs.remove(0);
348         if (pair.mAction == Transaction.ACTION_IO_EXCEPTION) {
349             throw new IOException("Expected IOException.");
350         }
351         SmtpSenderUnitTests.assertTrue("Unexpected string written to MockTransport: Actual=" + s
352                 + "  Expected=" + pair.mPattern,
353                 pair.mPattern != null && s.matches(pair.mPattern));
354         if (pair.mResponses != null) {
355             sendResponse(pair);
356         }
357     }
358 
359     /**
360      * This is an InputStream that satisfies the needs of getInputStream()
361      */
362     private class MockInputStream extends InputStream {
363 
364         private byte[] mNextLine = null;
365         private int mNextIndex = 0;
366 
367         /**
368          * Reads from the same input buffer as readLine()
369          */
370         @Override
read()371         public int read() throws IOException {
372             if (!mInputOpen) {
373                 throw new IOException();
374             }
375 
376             if (mNextLine != null && mNextIndex < mNextLine.length) {
377                 return mNextLine[mNextIndex++];
378             }
379 
380             // previous line was exhausted so try to get another one
381             String next = readLine();
382             if (next == null) {
383                 throw new IOException("Reading from MockTransport with closed input");
384             }
385             mNextLine = (next + "\r\n").getBytes();
386             mNextIndex = 0;
387 
388             if (mNextLine != null && mNextIndex < mNextLine.length) {
389                 return mNextLine[mNextIndex++];
390             }
391 
392             // no joy - throw an exception
393             throw new IOException();
394         }
395     }
396 
397     /**
398      * This is an OutputStream that satisfies the needs of getOutputStream()
399      */
400     private class MockOutputStream extends OutputStream {
401 
402         private StringBuilder sb = new StringBuilder();
403 
404         @Override
write(int oneByte)405         public void write(int oneByte) throws IOException {
406             // CR or CRLF will immediately dump previous line (w/o CRLF)
407             if (oneByte == '\r') {
408                 writeLine(sb.toString(), null);
409                 sb = new StringBuilder();
410             } else if (oneByte == '\n') {
411                 // swallow it
412             } else {
413                 sb.append((char)oneByte);
414             }
415         }
416     }
417 
418     @Override
getLocalAddress()419     public InetAddress getLocalAddress() {
420         if (isOpen()) {
421             return mLocalAddress;
422         } else {
423             return null;
424         }
425     }
426 }
427