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.net.URI; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.regex.Pattern; 31 32 import junit.framework.Assert; 33 34 /** 35 * This is a mock Transport that is used to test protocols that use MailTransport. 36 */ 37 public 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 mTlsStarted = false; 48 49 private boolean mOpen; 50 private boolean mInputOpen; 51 private int mConnectionSecurity; 52 private boolean mTrustCertificates; 53 private String mHost; 54 private InetAddress mLocalAddress; 55 56 private ArrayList<String> mQueuedInput = new ArrayList<String>(); 57 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; 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 case ACTION_START_TLS: 90 return "Expect StartTls"; 91 default: 92 return "(Hmm. Unknown action.)"; 93 } 94 } 95 } 96 97 private ArrayList<Transaction> mPairs = new ArrayList<Transaction>(); 98 99 /** 100 * Give the mock a pattern to wait for. No response will be sent. 101 * @param pattern Java RegEx to wait for 102 */ 103 public void expect(String pattern) { 104 expect(pattern, (String[])null); 105 } 106 107 /** 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 111 */ 112 public void expect(String pattern, String response) { 113 expect(pattern, (response == null) ? null : new String[] { response }); 114 } 115 116 /** 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 120 */ 121 public void expect(String pattern, String[] responses) { 122 Transaction pair = new Transaction(pattern, responses); 123 mPairs.add(pair); 124 } 125 126 /** 127 * Same as {@link #expect(String, String[])}, but the first arg is taken literally, rather than 128 * as a regexp. 129 */ 130 public void expectLiterally(String literal, String[] responses) { 131 expect("^" + Pattern.quote(literal) + "$", responses); 132 } 133 134 /** 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). 138 */ 139 public void expectClose() { 140 mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE)); 141 } 142 143 public void expectIOException() { 144 mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION)); 145 } 146 147 public void expectStartTls() { 148 mPairs.add(new Transaction(Transaction.ACTION_START_TLS)); 149 } 150 151 private void sendResponse(Transaction pair) { 152 switch (pair.mAction) { 153 case Transaction.ACTION_INJECT_TEXT: 154 for (String s : pair.mResponses) { 155 mQueuedInput.add(s); 156 } 157 break; 158 case Transaction.ACTION_IO_EXCEPTION: 159 mQueuedInput.add(SPECIAL_RESPONSE_IOEXCEPTION); 160 break; 161 default: 162 Assert.fail("Invalid action for sendResponse: " + pair.mAction); 163 } 164 } 165 166 public boolean canTrySslSecurity() { 167 return (mConnectionSecurity == CONNECTION_SECURITY_SSL); 168 } 169 170 public boolean canTryTlsSecurity() { 171 return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS); 172 } 173 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 public void close() { 194 mOpen = false; 195 mInputOpen = false; 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) { 200 return; 201 } 202 } 203 mQueuedInput.clear(); 204 mPairs.clear(); 205 } 206 207 /** 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. 210 */ 211 public void setMockHost(String host) { 212 mHost = host; 213 } 214 215 public String getHost() { 216 return mHost; 217 } 218 219 public void setMockLocalAddress(InetAddress address) { 220 mLocalAddress = address; 221 } 222 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 public Transport newInstanceWithConfiguration() { 234 return this; 235 } 236 237 public OutputStream getOutputStream() { 238 Assert.assertTrue(mOpen); 239 return new MockOutputStream(); 240 } 241 242 public int getPort() { 243 SmtpSenderUnitTests.fail("getPort() not implemented"); 244 return 0; 245 } 246 247 public int getSecurity() { 248 return mConnectionSecurity; 249 } 250 251 public String[] getUserInfoParts() { 252 SmtpSenderUnitTests.fail("getUserInfoParts() not implemented"); 253 return null; 254 } 255 256 public boolean isOpen() { 257 return mOpen; 258 } 259 260 public void open() /* throws MessagingException, CertificateValidationException */ { 261 mOpen = true; 262 mInputOpen = true; 263 } 264 265 /** 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. 270 * 271 * If no response text is available, we assert (failing our test) as an underflow. 272 * 273 * Logs the read text if DEBUG_LOG_STREAMS is true. 274 */ 275 public String readLine() throws IOException { 276 SmtpSenderUnitTests.assertTrue(mOpen); 277 if (!mInputOpen) { 278 throw new IOException("Reading from MockTransport with closed input"); 279 } 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) { 284 mPairs.remove(0); 285 sendResponse(pair); 286 } 287 } 288 if (mQueuedInput.size() == 0) { 289 // MailTransport returns "" at EOS. 290 Log.w(LOG_TAG, "Underflow reading from MockTransport"); 291 return ""; 292 } 293 String line = mQueuedInput.remove(0); 294 if (DEBUG_LOG_STREAMS) { 295 Log.d(LOG_TAG, "<<< " + line); 296 } 297 if (SPECIAL_RESPONSE_IOEXCEPTION.equals(line)) { 298 throw new IOException("Expected IOException."); 299 } 300 return line; 301 } 302 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); 307 mTlsStarted = true; 308 } 309 310 public void setSecurity(int connectionSecurity, boolean trustAllCertificates) { 311 mConnectionSecurity = connectionSecurity; 312 mTrustCertificates = trustAllCertificates; 313 } 314 315 public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ { 316 } 317 318 public void setUri(URI uri, int defaultPort) { 319 SmtpSenderUnitTests.assertTrue("Don't call setUri on a mock transport", false); 320 } 321 322 /** 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. 328 * 329 * Logs the written text if DEBUG_LOG_STREAMS is true. 330 */ 331 public void writeLine(String s, String sensitiveReplacement) throws IOException { 332 if (DEBUG_LOG_STREAMS) { 333 Log.d(LOG_TAG, ">>> " + s); 334 } 335 SmtpSenderUnitTests.assertTrue(mOpen); 336 SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s, 337 0 != mPairs.size()); 338 Transaction pair = mPairs.remove(0); 339 if (pair.mAction == Transaction.ACTION_IO_EXCEPTION) { 340 throw new IOException("Expected IOException."); 341 } 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) { 346 sendResponse(pair); 347 } 348 } 349 350 /** 351 * This is an InputStream that satisfies the needs of getInputStream() 352 */ 353 private class MockInputStream extends InputStream { 354 355 byte[] mNextLine = null; 356 int mNextIndex = 0; 357 358 /** 359 * Reads from the same input buffer as readLine() 360 */ 361 @Override 362 public int read() throws IOException { 363 if (!mInputOpen) { 364 throw new IOException(); 365 } 366 367 if (mNextLine != null && mNextIndex < mNextLine.length) { 368 return mNextLine[mNextIndex++]; 369 } 370 371 // previous line was exhausted so try to get another one 372 String next = readLine(); 373 if (next == null) { 374 throw new IOException("Reading from MockTransport with closed input"); 375 } 376 mNextLine = (next + "\r\n").getBytes(); 377 mNextIndex = 0; 378 379 if (mNextLine != null && mNextIndex < mNextLine.length) { 380 return mNextLine[mNextIndex++]; 381 } 382 383 // no joy - throw an exception 384 throw new IOException(); 385 } 386 } 387 388 /** 389 * This is an OutputStream that satisfies the needs of getOutputStream() 390 */ 391 private class MockOutputStream extends OutputStream { 392 393 StringBuilder sb = new StringBuilder(); 394 395 @Override 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') { 402 // swallow it 403 } else { 404 sb.append((char)oneByte); 405 } 406 } 407 } 408 409 public InetAddress getLocalAddress() { 410 if (isOpen()) { 411 return mLocalAddress; 412 } else { 413 return null; 414 } 415 } 416 }