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