2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.email.mail.transport;
19 import com.android.email.mail.Transport;
21 import android.util.Log;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.net.InetAddress;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.regex.Pattern;
32 import junit.framework.Assert;
35 * This is a mock Transport that is used to test protocols that use MailTransport.
37 public class MockTransport implements Transport {
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;
43 private static String LOG_TAG = "MockTransport";
45 private static final String SPECIAL_RESPONSE_IOEXCEPTION = "!!!IOEXCEPTION!!!";
47 private boolean mTlsStarted = false;
49 private boolean mOpen;
50 private boolean mInputOpen;
51 private int mConnectionSecurity;
52 private boolean mTrustCertificates;
54 private InetAddress mLocalAddress;
56 private ArrayList<String> mQueuedInput = new ArrayList<String>();
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;
68 Transaction(String pattern, String[] responses) {
69 mAction = ACTION_INJECT_TEXT;
71 mResponses = responses;
74 Transaction(int otherType) {
81 public String toString() {
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";
92 return "(Hmm. Unknown action.)";
97 private ArrayList<Transaction> mPairs = new ArrayList<Transaction>();
100 * Give the mock a pattern to wait for. No response will be sent.
101 * @param pattern Java RegEx to wait for
103 public void expect(String pattern) {
104 expect(pattern, (String[])null);
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
112 public void expect(String pattern, String response) {
113 expect(pattern, (response == null) ? null : new String[] { response });
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
121 public void expect(String pattern, String[] responses) {
122 Transaction pair = new Transaction(pattern, responses);
127 * Same as {@link #expect(String, String[])}, but the first arg is taken literally, rather than
130 public void expectLiterally(String literal, String[] responses) {
131 expect("^" + Pattern.quote(literal) + "$", responses);
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).
139 public void expectClose() {
140 mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE));
143 public void expectIOException() {
144 mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
147 public void expectStartTls() {
148 mPairs.add(new Transaction(Transaction.ACTION_START_TLS));
151 private void sendResponse(Transaction pair) {
152 switch (pair.mAction) {
153 case Transaction.ACTION_INJECT_TEXT:
154 for (String s : pair.mResponses) {
158 case Transaction.ACTION_IO_EXCEPTION:
159 mQueuedInput.add(SPECIAL_RESPONSE_IOEXCEPTION);
162 Assert.fail("Invalid action for sendResponse: " + pair.mAction);
166 public boolean canTrySslSecurity() {
167 return (mConnectionSecurity == CONNECTION_SECURITY_SSL);
170 public boolean canTryTlsSecurity() {
171 return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS);
174 public boolean canTrustAllCertificates() {
175 return mTrustCertificates;
179 * Check that TLS was started
181 public boolean isTlsStarted() {
186 * This simulates a condition where the server has closed its side, causing
189 public void closeInputStream() {
193 public void close() {
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) {
203 mQueuedInput.clear();
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.
211 public void setMockHost(String host) {
215 public String getHost() {
219 public void setMockLocalAddress(InetAddress address) {
220 mLocalAddress = address;
223 public InputStream getInputStream() {
224 SmtpSenderUnitTests.assertTrue(mOpen);
225 return new MockInputStream();
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.
233 public Transport newInstanceWithConfiguration() {
237 public OutputStream getOutputStream() {
238 Assert.assertTrue(mOpen);
239 return new MockOutputStream();
242 public int getPort() {
243 SmtpSenderUnitTests.fail("getPort() not implemented");
247 public int getSecurity() {
248 return mConnectionSecurity;
251 public String[] getUserInfoParts() {
252 SmtpSenderUnitTests.fail("getUserInfoParts() not implemented");
256 public boolean isOpen() {
260 public void open() /* throws MessagingException, CertificateValidationException */ {
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.
271 * If no response text is available, we assert (failing our test) as an underflow.
273 * Logs the read text if DEBUG_LOG_STREAMS is true.
275 public String readLine() throws IOException {
276 SmtpSenderUnitTests.assertTrue(mOpen);
278 throw new IOException("Reading from MockTransport with closed input");
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) {
288 if (mQueuedInput.size() == 0) {
289 // MailTransport returns "" at EOS.
290 Log.w(LOG_TAG, "Underflow reading from MockTransport");
293 String line = mQueuedInput.remove(0);
294 if (DEBUG_LOG_STREAMS) {
295 Log.d(LOG_TAG, "<<< " + line);
297 if (SPECIAL_RESPONSE_IOEXCEPTION.equals(line)) {
298 throw new IOException("Expected IOException.");
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);
310 public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
311 mConnectionSecurity = connectionSecurity;
312 mTrustCertificates = trustAllCertificates;
315 public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ {
318 public void setUri(URI uri, int defaultPort) {
319 SmtpSenderUnitTests.assertTrue("Don't call setUri on a mock transport", false);
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.
329 * Logs the written text if DEBUG_LOG_STREAMS is true.
331 public void writeLine(String s, String sensitiveReplacement) throws IOException {
332 if (DEBUG_LOG_STREAMS) {
333 Log.d(LOG_TAG, ">>> " + s);
335 SmtpSenderUnitTests.assertTrue(mOpen);
336 SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s,
338 Transaction pair = mPairs.remove(0);
339 if (pair.mAction == Transaction.ACTION_IO_EXCEPTION) {
340 throw new IOException("Expected IOException.");
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) {
351 * This is an InputStream that satisfies the needs of getInputStream()
353 private class MockInputStream extends InputStream {
355 byte[] mNextLine = null;
359 * Reads from the same input buffer as readLine()
362 public int read() throws IOException {
364 throw new IOException();
367 if (mNextLine != null && mNextIndex < mNextLine.length) {
368 return mNextLine[mNextIndex++];
371 // previous line was exhausted so try to get another one
372 String next = readLine();
374 throw new IOException("Reading from MockTransport with closed input");
376 mNextLine = (next + "\r\n").getBytes();
379 if (mNextLine != null && mNextIndex < mNextLine.length) {
380 return mNextLine[mNextIndex++];
383 // no joy - throw an exception
384 throw new IOException();
389 * This is an OutputStream that satisfies the needs of getOutputStream()
391 private class MockOutputStream extends OutputStream {
393 StringBuilder sb = new StringBuilder();
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') {
404 sb.append((char)oneByte);
409 public InetAddress getLocalAddress() {
411 return mLocalAddress;