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