Home | History | Annotate | Download | only in transport
      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 
     67         Transaction(String pattern, String[] responses) {
     68             mAction = ACTION_INJECT_TEXT;
     69             mPattern = pattern;
     70             mResponses = responses;
     71         }
     72 
     73         Transaction(int otherType) {
     74             mAction = otherType;
     75             mPattern = null;
     76             mResponses = null;
     77         }
     78 
     79         @Override
     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      */
    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      */
    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      */
    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      */
    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      */
    138     public void expectClose() {
    139         mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE));
    140     }
    141 
    142     public void expectIOException() {
    143         mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
    144     }
    145 
    146     public void expectStartTls() {
    147         mPairs.add(new Transaction(Transaction.ACTION_START_TLS));
    148     }
    149 
    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
    166     public boolean canTrySslSecurity() {
    167         return (mConnectionSecurity == CONNECTION_SECURITY_SSL);
    168     }
    169 
    170     @Override
    171     public boolean canTryTlsSecurity() {
    172         return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS);
    173     }
    174 
    175     @Override
    176     public boolean canTrustAllCertificates() {
    177         return mTrustCertificates;
    178     }
    179 
    180     /**
    181      * Check that TLS was started
    182      */
    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      */
    191     public void closeInputStream() {
    192         mInputOpen = false;
    193     }
    194 
    195     @Override
    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
    211     public void setHost(String host) {
    212         mHost = host;
    213     }
    214 
    215     @Override
    216     public String getHost() {
    217         return mHost;
    218     }
    219 
    220     public void setMockLocalAddress(InetAddress address) {
    221         mLocalAddress = address;
    222     }
    223 
    224     @Override
    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
    236     public Transport clone() {
    237          return this;
    238     }
    239 
    240     @Override
    241     public OutputStream getOutputStream() {
    242         Assert.assertTrue(mOpen);
    243         return new MockOutputStream();
    244     }
    245 
    246     @Override
    247     public void setPort(int port) {
    248         SmtpSenderUnitTests.fail("setPort() not implemented");
    249     }
    250 
    251     @Override
    252     public int getPort() {
    253         SmtpSenderUnitTests.fail("getPort() not implemented");
    254         return 0;
    255     }
    256 
    257     @Override
    258     public int getSecurity() {
    259         return mConnectionSecurity;
    260     }
    261 
    262     @Override
    263     public boolean isOpen() {
    264         return mOpen;
    265     }
    266 
    267     @Override
    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
    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
    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
    321     public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
    322         mConnectionSecurity = connectionSecurity;
    323         mTrustCertificates = trustAllCertificates;
    324     }
    325 
    326     @Override
    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
    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
    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
    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
    419     public InetAddress getLocalAddress() {
    420         if (isOpen()) {
    421             return mLocalAddress;
    422         } else {
    423             return null;
    424         }
    425     }
    426 }
    427