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 java.io.IOException;
     22 import java.io.InputStream;
     23 import java.io.OutputStream;
     24 import java.net.InetAddress;
     25 import java.util.ArrayList;
     26 import java.util.Arrays;
     27 import java.util.regex.Pattern;
     28 
     29 import junit.framework.Assert;
     30 
     31 /**
     32  * This is a mock Transport that is used to test protocols that use MailTransport.
     33  */
     34 public class MockTransport implements Transport {
     35 
     36     // All flags defining debug or development code settings must be FALSE
     37     // when code is checked in or released.
     38     private static boolean DEBUG_LOG_STREAMS = true;
     39 
     40     private static String LOG_TAG = "MockTransport";
     41 
     42     private static final String SPECIAL_RESPONSE_IOEXCEPTION = "!!!IOEXCEPTION!!!";
     43 
     44     private boolean mTlsStarted = false;
     45 
     46     private boolean mOpen;
     47     private boolean mInputOpen;
     48     private int mConnectionSecurity;
     49     private boolean mTrustCertificates;
     50     private String mHost;
     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     /**
     97      * Give the mock a pattern to wait for.  No response will be sent.
     98      * @param pattern Java RegEx to wait for
     99      */
    100     public void expect(String pattern) {
    101         expect(pattern, (String[])null);
    102     }
    103 
    104     /**
    105      * Give the mock a pattern to wait for and a response to send back.
    106      * @param pattern Java RegEx to wait for
    107      * @param response String to reply with, or null to acccept string but not respond to it
    108      */
    109     public void expect(String pattern, String response) {
    110         expect(pattern, (response == null) ? null : new String[] { response });
    111     }
    112 
    113     /**
    114      * Give the mock a pattern to wait for and a multi-line response to send back.
    115      * @param pattern Java RegEx to wait for
    116      * @param responses Strings to reply with
    117      */
    118     public void expect(String pattern, String[] responses) {
    119         Transaction pair = new Transaction(pattern, responses);
    120         mPairs.add(pair);
    121     }
    122 
    123     /**
    124      * Same as {@link #expect(String, String[])}, but the first arg is taken literally, rather than
    125      * as a regexp.
    126      */
    127     public void expectLiterally(String literal, String[] responses) {
    128         expect("^" + Pattern.quote(literal) + "$", responses);
    129     }
    130 
    131     /**
    132      * Tell the Mock Transport that we expect it to be closed.  This will preserve
    133      * the remaining entries in the expect() stream and allow us to "ride over" the close (which
    134      * would normally reset everything).
    135      */
    136     public void expectClose() {
    137         mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE));
    138     }
    139 
    140     public void expectIOException() {
    141         mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
    142     }
    143 
    144     public void expectStartTls() {
    145         mPairs.add(new Transaction(Transaction.ACTION_START_TLS));
    146     }
    147 
    148     private void sendResponse(Transaction pair) {
    149         switch (pair.mAction) {
    150             case Transaction.ACTION_INJECT_TEXT:
    151                 for (String s : pair.mResponses) {
    152                     mQueuedInput.add(s);
    153                 }
    154                 break;
    155             case Transaction.ACTION_IO_EXCEPTION:
    156                 mQueuedInput.add(SPECIAL_RESPONSE_IOEXCEPTION);
    157                 break;
    158             default:
    159                 Assert.fail("Invalid action for sendResponse: " + pair.mAction);
    160         }
    161     }
    162 
    163     @Override
    164     public boolean canTrySslSecurity() {
    165         return (mConnectionSecurity == CONNECTION_SECURITY_SSL);
    166     }
    167 
    168     @Override
    169     public boolean canTryTlsSecurity() {
    170         return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS);
    171     }
    172 
    173     @Override
    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     @Override
    194     public void close() {
    195         mOpen = false;
    196         mInputOpen = false;
    197         // unless it was expected as part of a test, reset the stream
    198         if (mPairs.size() > 0) {
    199             Transaction expect = mPairs.remove(0);
    200             if (expect.mAction == Transaction.ACTION_CLIENT_CLOSE) {
    201                 return;
    202             }
    203         }
    204         mQueuedInput.clear();
    205         mPairs.clear();
    206     }
    207 
    208     @Override
    209     public void setHost(String host) {
    210         mHost = host;
    211     }
    212 
    213     @Override
    214     public String getHost() {
    215         return mHost;
    216     }
    217 
    218     public void setMockLocalAddress(InetAddress address) {
    219         mLocalAddress = address;
    220     }
    221 
    222     @Override
    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     @Override
    234     public Transport clone() {
    235          return this;
    236     }
    237 
    238     @Override
    239     public OutputStream getOutputStream() {
    240         Assert.assertTrue(mOpen);
    241         return new MockOutputStream();
    242     }
    243 
    244     @Override
    245     public void setPort(int port) {
    246         SmtpSenderUnitTests.fail("setPort() not implemented");
    247     }
    248 
    249     @Override
    250     public int getPort() {
    251         SmtpSenderUnitTests.fail("getPort() not implemented");
    252         return 0;
    253     }
    254 
    255     @Override
    256     public int getSecurity() {
    257         return mConnectionSecurity;
    258     }
    259 
    260     @Override
    261     public boolean isOpen() {
    262         return mOpen;
    263     }
    264 
    265     @Override
    266     public void open() /* throws MessagingException, CertificateValidationException */ {
    267         mOpen = true;
    268         mInputOpen = true;
    269     }
    270 
    271     /**
    272      * This returns one string (if available) to the caller.  Usually this simply pulls strings
    273      * from the mQueuedInput list, but if the list is empty, we also peek the expect list.  This
    274      * supports banners, multi-line responses, and any other cases where we respond without
    275      * a specific expect pattern.
    276      *
    277      * If no response text is available, we assert (failing our test) as an underflow.
    278      *
    279      * Logs the read text if DEBUG_LOG_STREAMS is true.
    280      */
    281     @Override
    282     public String readLine() throws IOException {
    283         SmtpSenderUnitTests.assertTrue(mOpen);
    284         if (!mInputOpen) {
    285             throw new IOException("Reading from MockTransport with closed input");
    286         }
    287         // if there's nothing to read, see if we can find a null-pattern response
    288         if ((mQueuedInput.size() == 0) && (mPairs.size() > 0)) {
    289             Transaction pair = mPairs.get(0);
    290             if (pair.mPattern == null) {
    291                 mPairs.remove(0);
    292                 sendResponse(pair);
    293             }
    294         }
    295         if (mQueuedInput.size() == 0) {
    296             // MailTransport returns "" at EOS.
    297             LogUtils.w(LOG_TAG, "Underflow reading from MockTransport");
    298             return "";
    299         }
    300         String line = mQueuedInput.remove(0);
    301         if (DEBUG_LOG_STREAMS) {
    302             LogUtils.d(LOG_TAG, "<<< " + line);
    303         }
    304         if (SPECIAL_RESPONSE_IOEXCEPTION.equals(line)) {
    305             throw new IOException("Expected IOException.");
    306         }
    307         return line;
    308     }
    309 
    310     @Override
    311     public void reopenTls() /* throws MessagingException */ {
    312         SmtpSenderUnitTests.assertTrue(mOpen);
    313         Transaction expect = mPairs.remove(0);
    314         SmtpSenderUnitTests.assertTrue(expect.mAction == Transaction.ACTION_START_TLS);
    315         mTlsStarted = true;
    316     }
    317 
    318     @Override
    319     public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
    320         mConnectionSecurity = connectionSecurity;
    321         mTrustCertificates = trustAllCertificates;
    322     }
    323 
    324     @Override
    325     public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ {
    326     }
    327 
    328     /**
    329      * Accepts a single string (command or text) that was written by the code under test.
    330      * Because we are essentially mocking a server, we check to see if this string was expected.
    331      * If the string was expected, we push the corresponding responses into the mQueuedInput
    332      * list, for subsequent calls to readLine().  If the string does not match, we assert
    333      * the mismatch.  If no string was expected, we assert it as an overflow.
    334      *
    335      * Logs the written text if DEBUG_LOG_STREAMS is true.
    336      */
    337     @Override
    338     public void writeLine(String s, String sensitiveReplacement) throws IOException {
    339         if (DEBUG_LOG_STREAMS) {
    340             LogUtils.d(LOG_TAG, ">>> " + s);
    341         }
    342         SmtpSenderUnitTests.assertTrue(mOpen);
    343         SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s,
    344                 0 != mPairs.size());
    345         Transaction pair = mPairs.remove(0);
    346         if (pair.mAction == Transaction.ACTION_IO_EXCEPTION) {
    347             throw new IOException("Expected IOException.");
    348         }
    349         SmtpSenderUnitTests.assertTrue("Unexpected string written to MockTransport: Actual=" + s
    350                 + "  Expected=" + pair.mPattern,
    351                 pair.mPattern != null && s.matches(pair.mPattern));
    352         if (pair.mResponses != null) {
    353             sendResponse(pair);
    354         }
    355     }
    356 
    357     /**
    358      * This is an InputStream that satisfies the needs of getInputStream()
    359      */
    360     private class MockInputStream extends InputStream {
    361 
    362         private byte[] mNextLine = null;
    363         private int mNextIndex = 0;
    364 
    365         /**
    366          * Reads from the same input buffer as readLine()
    367          */
    368         @Override
    369         public int read() throws IOException {
    370             if (!mInputOpen) {
    371                 throw new IOException();
    372             }
    373 
    374             if (mNextLine != null && mNextIndex < mNextLine.length) {
    375                 return mNextLine[mNextIndex++];
    376             }
    377 
    378             // previous line was exhausted so try to get another one
    379             String next = readLine();
    380             if (next == null) {
    381                 throw new IOException("Reading from MockTransport with closed input");
    382             }
    383             mNextLine = (next + "\r\n").getBytes();
    384             mNextIndex = 0;
    385 
    386             if (mNextLine != null && mNextIndex < mNextLine.length) {
    387                 return mNextLine[mNextIndex++];
    388             }
    389 
    390             // no joy - throw an exception
    391             throw new IOException();
    392         }
    393     }
    394 
    395     /**
    396      * This is an OutputStream that satisfies the needs of getOutputStream()
    397      */
    398     private class MockOutputStream extends OutputStream {
    399 
    400         private StringBuilder sb = new StringBuilder();
    401 
    402         @Override
    403         public void write(int oneByte) throws IOException {
    404             // CR or CRLF will immediately dump previous line (w/o CRLF)
    405             if (oneByte == '\r') {
    406                 writeLine(sb.toString(), null);
    407                 sb = new StringBuilder();
    408             } else if (oneByte == '\n') {
    409                 // swallow it
    410             } else {
    411                 sb.append((char)oneByte);
    412             }
    413         }
    414     }
    415 
    416     @Override
    417     public InetAddress getLocalAddress() {
    418         if (isOpen()) {
    419             return mLocalAddress;
    420         } else {
    421             return null;
    422         }
    423     }
    424 }
    425