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