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