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