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