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