/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.email.mail.transport; import com.android.email.mail.Transport; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.regex.Pattern; import junit.framework.Assert; /** * This is a mock Transport that is used to test protocols that use MailTransport. */ public class MockTransport implements Transport { // All flags defining debug or development code settings must be FALSE // when code is checked in or released. private static boolean DEBUG_LOG_STREAMS = true; private static String LOG_TAG = "MockTransport"; private boolean mSslAllowed = false; private boolean mTlsAllowed = false; private boolean mTlsReopened = false; private boolean mOpen; private boolean mInputOpen; private int mConnectionSecurity; private boolean mTrustCertificates; private String mHost; private ArrayList<String> mQueuedInput = new ArrayList<String>(); private static class Transaction { public static final int ACTION_INJECT_TEXT = 0; public static final int ACTION_SERVER_CLOSE = 1; public static final int ACTION_CLIENT_CLOSE = 2; int mAction; String mPattern; String[] mResponses; Transaction(String pattern, String[] responses) { mAction = ACTION_INJECT_TEXT; mPattern = pattern; mResponses = responses; } Transaction(int otherType) { mAction = otherType; mPattern = null; mResponses = null; } @Override public String toString() { switch (mAction) { case ACTION_INJECT_TEXT: return mPattern + ": " + Arrays.toString(mResponses); case ACTION_SERVER_CLOSE: return "Close the server connection"; case ACTION_CLIENT_CLOSE: return "Expect the client to close"; default: return "(Hmm. Unknown action.)"; } } } private ArrayList<Transaction> mPairs = new ArrayList<Transaction>(); /** * Give the mock a pattern to wait for. No response will be sent. * @param pattern Java RegEx to wait for */ public void expect(String pattern) { expect(pattern, (String[])null); } /** * Give the mock a pattern to wait for and a response to send back. * @param pattern Java RegEx to wait for * @param response String to reply with, or null to acccept string but not respond to it */ public void expect(String pattern, String response) { expect(pattern, (response == null) ? null : new String[] { response }); } /** * Give the mock a pattern to wait for and a multi-line response to send back. * @param pattern Java RegEx to wait for * @param responses Strings to reply with */ public void expect(String pattern, String[] responses) { Transaction pair = new Transaction(pattern, responses); mPairs.add(pair); } /** * Same as {@link #expect(String, String[])}, but the first arg is taken literally, rather than * as a regexp. */ public void expectLiterally(String literal, String[] responses) { expect("^" + Pattern.quote(literal) + "$", responses); } /** * Tell the Mock Transport that we expect it to be closed. This will preserve * the remaining entries in the expect() stream and allow us to "ride over" the close (which * would normally reset everything). */ public void expectClose() { mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE)); } private void sendResponse(String[] responses) { for (String s : responses) { mQueuedInput.add(s); } } public boolean canTrySslSecurity() { return (mConnectionSecurity == CONNECTION_SECURITY_SSL); } public boolean canTryTlsSecurity() { return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS); } public boolean canTrustAllCertificates() { return mTrustCertificates; } /** * This simulates a condition where the server has closed its side, causing * reads to fail. */ public void closeInputStream() { mInputOpen = false; } public void close() { mOpen = false; mInputOpen = false; // unless it was expected as part of a test, reset the stream if (mPairs.size() > 0) { Transaction expect = mPairs.remove(0); if (expect.mAction == Transaction.ACTION_CLIENT_CLOSE) { return; } } mQueuedInput.clear(); mPairs.clear(); } /** * This is a test function (not part of the interface) and is used to set up a result * value for getHost(), if needed for the test. */ public void setMockHost(String host) { mHost = host; } public String getHost() { return mHost; } public InputStream getInputStream() { SmtpSenderUnitTests.assertTrue(mOpen); return new MockInputStream(); } /** * This normally serves as a pseudo-clone, for use by Imap. For the purposes of unit testing, * until we need something more complex, we'll just return the actual MockTransport. Then we * don't have to worry about dealing with test metadata like the expects list or socket state. */ public Transport newInstanceWithConfiguration() { return this; } public OutputStream getOutputStream() { Assert.assertTrue(mOpen); return new MockOutputStream(); } public int getPort() { SmtpSenderUnitTests.fail("getPort() not implemented"); return 0; } public int getSecurity() { return mConnectionSecurity; } public String[] getUserInfoParts() { SmtpSenderUnitTests.fail("getUserInfoParts() not implemented"); return null; } public boolean isOpen() { return mOpen; } public void open() /* throws MessagingException, CertificateValidationException */ { mOpen = true; mInputOpen = true; } /** * This returns one string (if available) to the caller. Usually this simply pulls strings * from the mQueuedInput list, but if the list is empty, we also peek the expect list. This * supports banners, multi-line responses, and any other cases where we respond without * a specific expect pattern. * * If no response text is available, we assert (failing our test) as an underflow. * * Logs the read text if DEBUG_LOG_STREAMS is true. */ public String readLine() throws IOException { SmtpSenderUnitTests.assertTrue(mOpen); if (!mInputOpen) { throw new IOException("Reading from MockTransport with closed input"); } // if there's nothing to read, see if we can find a null-pattern response if (0 == mQueuedInput.size()) { Transaction pair = mPairs.get(0); if (pair != null && pair.mPattern == null) { mPairs.remove(0); sendResponse(pair.mResponses); } } SmtpSenderUnitTests.assertTrue("Underflow reading from MockTransport", 0 != mQueuedInput.size()); String line = mQueuedInput.remove(0); if (DEBUG_LOG_STREAMS) { Log.d(LOG_TAG, "<<< " + line); } return line; } public void setTlsAllowed(boolean tlsAllowed) { mTlsAllowed = tlsAllowed; } public boolean getTlsAllowed() { return mTlsAllowed; } public void setSslAllowed(boolean sslAllowed) { mSslAllowed = sslAllowed; } public boolean getSslAllowed() { return mSslAllowed; } public void reopenTls() { SmtpSenderUnitTests.assertTrue(mOpen); SmtpSenderUnitTests.assertTrue(mTlsAllowed); mTlsReopened = true; } public boolean getTlsReopened() { return mTlsReopened; } public void setSecurity(int connectionSecurity, boolean trustAllCertificates) { mConnectionSecurity = connectionSecurity; mTrustCertificates = trustAllCertificates; } public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ { } public void setUri(URI uri, int defaultPort) { SmtpSenderUnitTests.assertTrue("Don't call setUri on a mock transport", false); } /** * Accepts a single string (command or text) that was written by the code under test. * Because we are essentially mocking a server, we check to see if this string was expected. * If the string was expected, we push the corresponding responses into the mQueuedInput * list, for subsequent calls to readLine(). If the string does not match, we assert * the mismatch. If no string was expected, we assert it as an overflow. * * Logs the written text if DEBUG_LOG_STREAMS is true. */ public void writeLine(String s, String sensitiveReplacement) /* throws IOException */ { if (DEBUG_LOG_STREAMS) { Log.d(LOG_TAG, ">>> " + s); } SmtpSenderUnitTests.assertTrue(mOpen); SmtpSenderUnitTests.assertTrue("Overflow writing to MockTransport: Getting " + s, 0 != mPairs.size()); Transaction pair = mPairs.remove(0); SmtpSenderUnitTests.assertTrue("Unexpected string written to MockTransport: Actual=" + s + " Expected=" + pair.mPattern, pair.mPattern != null && s.matches(pair.mPattern)); if (pair.mResponses != null) { sendResponse(pair.mResponses); } } /** * This is an InputStream that satisfies the needs of getInputStream() */ private class MockInputStream extends InputStream { byte[] mNextLine = null; int mNextIndex = 0; /** * Reads from the same input buffer as readLine() */ @Override public int read() throws IOException { if (!mInputOpen) { throw new IOException(); } if (mNextLine != null && mNextIndex < mNextLine.length) { return mNextLine[mNextIndex++]; } // previous line was exhausted so try to get another one String next = readLine(); if (next == null) { throw new IOException("Reading from MockTransport with closed input"); } mNextLine = (next + "\r\n").getBytes(); mNextIndex = 0; if (mNextLine != null && mNextIndex < mNextLine.length) { return mNextLine[mNextIndex++]; } // no joy - throw an exception throw new IOException(); } } /** * This is an OutputStream that satisfies the needs of getOutputStream() */ private class MockOutputStream extends OutputStream { StringBuilder sb = new StringBuilder(); @Override public void write(int oneByte) { // CR or CRLF will immediately dump previous line (w/o CRLF) if (oneByte == '\r') { writeLine(sb.toString(), null); sb = new StringBuilder(); } else if (oneByte == '\n') { // swallow it } else { sb.append((char)oneByte); } } } }