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