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