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