1/*
2 * Copyright (C) 2016 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.phone.common.mail;
18
19import static org.mockito.Mockito.mock;
20import static org.mockito.Mockito.verify;
21import static org.mockito.Mockito.when;
22
23import android.net.Network;
24import android.test.AndroidTestCase;
25
26import com.android.phone.MockitoHelper;
27import com.android.phone.common.mail.MailTransport.SocketCreator;
28import com.android.phone.common.mail.store.ImapStore;
29import com.android.phone.vvm.omtp.imap.ImapHelper;
30
31import junit.framework.AssertionFailedError;
32
33import org.mockito.MockitoAnnotations;
34
35import java.io.IOException;
36import java.io.InputStream;
37import java.io.OutputStream;
38import java.net.InetAddress;
39import java.net.InetSocketAddress;
40import java.net.Socket;
41import java.net.SocketAddress;
42import java.net.SocketException;
43import java.net.UnknownHostException;
44
45import javax.net.SocketFactory;
46
47public class MailTransportTest extends AndroidTestCase {
48
49    private static final String HOST_ADDRESS = "127.0.0.1";
50    private static final String INVALID_HOST_ADDRESS = "255.255.255.255";
51    private static final int HOST_PORT = 80;
52    private static final int HOST_FLAGS = 0;
53    // bypass verifyHostname() in open() by setting ImapStore.FLAG_TRUST_ALL
54    private static final int HOST_FLAGS_SSL = ImapStore.FLAG_SSL & ImapStore.FLAG_TRUST_ALL;
55    private static final InetAddress VALID_INET_ADDRESS = createInetAddress(HOST_ADDRESS);
56    private static final InetAddress INVALID_INET_ADDRESS = createInetAddress(INVALID_HOST_ADDRESS);
57
58    // ClassLoader need to be replaced for mockito to work.
59    private MockitoHelper mMokitoHelper = new MockitoHelper();
60
61    @Override
62    public void setUp() throws Exception {
63        super.setUp();
64        mMokitoHelper.setUp(getContext(), getClass());
65        MockitoAnnotations.initMocks(this);
66    }
67
68    @Override
69    public void tearDown() throws Exception {
70        mMokitoHelper.tearDown();
71        super.tearDown();
72    }
73
74    public void testCreateSocket_anyNetwork() throws MessagingException {
75        // With no network, Socket#Socket() should be called.
76        MailTransport transport =
77                new MailTransport(getContext(), createMockImapHelper(), null, HOST_ADDRESS,
78                        HOST_PORT, HOST_FLAGS);
79        Socket socket = transport.createSocket();
80        assertTrue(socket != null);
81    }
82
83    public void testCreateSocket_networkSpecified() throws MessagingException, IOException {
84        // Network#getSocketFactory should be used to create socket.
85        Network mockNetwork = createMockNetwork();
86        MailTransport transport =
87                new MailTransport(getContext(), createMockImapHelper(), mockNetwork, HOST_ADDRESS,
88                        HOST_PORT, HOST_FLAGS);
89        Socket socket = transport.createSocket();
90        assertTrue(socket != null);
91        verify(mockNetwork).getSocketFactory();
92    }
93
94    public void testCreateSocket_socketCreator() throws MessagingException, IOException {
95        // For testing purposes, how sockets are created can be overridden.
96        SocketCreator socketCreator = new SocketCreator() {
97
98            private final Socket mSocket = new Socket();
99
100            @Override
101            public Socket createSocket() {
102                return mSocket;
103            }
104        };
105
106        MailTransport transport = new
107                MailTransport(getContext(), createMockImapHelper(), null, HOST_ADDRESS, HOST_PORT,
108                HOST_FLAGS);
109
110        transport.setSocketCreator(socketCreator);
111
112        Socket socket = transport.createSocket();
113        assertTrue(socket == socketCreator.createSocket());
114    }
115
116    public void testOpen() throws MessagingException {
117        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), null,
118                HOST_ADDRESS,
119                HOST_PORT, HOST_FLAGS);
120        transport.setSocketCreator(new TestSocketCreator());
121        transport.open();
122        assertTrue(transport.isOpen());
123
124    }
125
126    public void testOpen_Ssl() throws MessagingException {
127        //opening with ssl support.
128        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), null,
129                HOST_ADDRESS, HOST_PORT, HOST_FLAGS_SSL);
130        transport.setSocketCreator(new TestSocketCreator());
131        transport.open();
132        assertTrue(transport.isOpen());
133
134    }
135
136    public void testOpen_MultiIp() throws MessagingException {
137        //In case of round robin DNS, try all resolved address until one succeeded.
138        Network network = createMultiIpMockNetwork();
139        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), network,
140                HOST_ADDRESS,
141                HOST_PORT, HOST_FLAGS);
142        transport.setSocketCreator(new TestSocketCreator());
143        transport.open();
144        assertTrue(transport.isOpen());
145    }
146
147    public void testOpen_MultiIp_SSL() throws MessagingException {
148        Network network = createMultiIpMockNetwork();
149
150        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), network,
151                HOST_ADDRESS,
152                HOST_PORT, HOST_FLAGS_SSL);
153        transport.setSocketCreator(new TestSocketCreator());
154        transport.open();
155        assertTrue(transport.isOpen());
156    }
157
158    public void testOpen_network_hostResolutionFailed() {
159        // Couldn't resolve host on the network. Open() should fail.
160        Network network = createMockNetwork();
161        try {
162            when(network.getAllByName(HOST_ADDRESS))
163                    .thenThrow(new UnknownHostException("host resolution failed"));
164        } catch (IOException e) {
165            //ignored
166        }
167
168        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), network,
169                HOST_ADDRESS,
170                HOST_PORT, HOST_FLAGS);
171        try {
172            transport.open();
173            throw new AssertionFailedError("Should throw MessagingException");
174        } catch (MessagingException e) {
175            //expected
176        }
177        assertFalse(transport.isOpen());
178    }
179
180    public void testOpen_createSocketFailed() {
181        // Unable to create socket. Open() should fail.
182        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), null,
183                HOST_ADDRESS,
184                HOST_PORT, HOST_FLAGS);
185        transport.setSocketCreator(new SocketCreator() {
186            @Override
187            public Socket createSocket() throws MessagingException {
188                throw new MessagingException("createSocket failed");
189            }
190        });
191        try {
192            transport.open();
193            throw new AssertionFailedError("Should throw MessagingException");
194        } catch (MessagingException e) {
195            //expected
196        }
197        assertFalse(transport.isOpen());
198    }
199
200    public void testOpen_network_createSocketFailed() {
201        // Unable to create socket. Open() should fail.
202
203        Network network = createOneIpMockNetwork();
204        SocketFactory mockSocketFactory = mock(SocketFactory.class);
205        try {
206            when(mockSocketFactory.createSocket())
207                    .thenThrow(new IOException("unable to create socket"));
208        } catch (IOException e) {
209            //ignored
210        }
211        when(network.getSocketFactory()).thenReturn(mockSocketFactory);
212
213        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), network,
214                HOST_ADDRESS, HOST_PORT, HOST_FLAGS);
215
216        try {
217            transport.open();
218            throw new AssertionFailedError("Should throw MessagingException");
219        } catch (MessagingException e) {
220            //expected
221        }
222        assertFalse(transport.isOpen());
223    }
224
225    public void testOpen_connectFailed_one() {
226        // There is only one IP for this host, and we failed to connect to it. Open() should fail.
227
228        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(), null,
229                HOST_ADDRESS, HOST_PORT, HOST_FLAGS);
230        transport.setSocketCreator(new SocketCreator() {
231            @Override
232            public Socket createSocket() throws MessagingException {
233                return new Socket() {
234                    @Override
235                    public void connect(SocketAddress address, int timeout) throws IOException {
236                        throw new IOException("connect failed");
237                    }
238                };
239            }
240        });
241        try {
242            transport.open();
243            throw new AssertionFailedError("Should throw MessagingException");
244        } catch (MessagingException e) {
245            //expected
246        }
247        assertFalse(transport.isOpen());
248    }
249
250    public void testOpen_connectFailed_multi() {
251        // There are multiple IP for this host, and we failed to connect to any of it.
252        // Open() should fail.
253        MailTransport transport = new MailTransport(getContext(), createMockImapHelper(),
254                createMultiIpMockNetwork(), HOST_ADDRESS, HOST_PORT, HOST_FLAGS);
255        transport.setSocketCreator(new SocketCreator() {
256            @Override
257            public Socket createSocket() throws MessagingException {
258                return new Socket() {
259                    @Override
260                    public void connect(SocketAddress address, int timeout) throws IOException {
261                        throw new IOException("connect failed");
262                    }
263                };
264            }
265        });
266        try {
267            transport.open();
268            throw new AssertionFailedError("Should throw MessagingException");
269        } catch (MessagingException e) {
270            //expected
271        }
272        assertFalse(transport.isOpen());
273    }
274
275    private class TestSocket extends Socket {
276
277        boolean mConnected = false;
278
279
280        /**
281         * A make a mock connection to the address.
282         *
283         * @param address Only address equivalent to VALID_INET_ADDRESS or INVALID_INET_ADDRESS is
284         * accepted
285         * @param timeout Ignored but should >= 0.
286         */
287        @Override
288        public void connect(SocketAddress address, int timeout) throws IOException {
289            // copied from Socket#connect
290            if (isClosed()) {
291                throw new SocketException("Socket is closed");
292            }
293            if (timeout < 0) {
294                throw new IllegalArgumentException("timeout < 0");
295            }
296            if (isConnected()) {
297                throw new SocketException("Already connected");
298            }
299            if (address == null) {
300                throw new IllegalArgumentException("remoteAddr == null");
301            }
302
303            if (!(address instanceof InetSocketAddress)) {
304                throw new AssertionError("address should be InetSocketAddress");
305            }
306
307            InetSocketAddress inetSocketAddress = (InetSocketAddress) address;
308            if (inetSocketAddress.getAddress().equals(INVALID_INET_ADDRESS)) {
309                throw new IOException("invalid address");
310            } else if (inetSocketAddress.getAddress().equals(VALID_INET_ADDRESS)) {
311                mConnected = true;
312            } else {
313                throw new AssertionError("Only INVALID_ADDRESS or VALID_ADDRESS are allowed");
314            }
315        }
316
317        @Override
318        public InputStream getInputStream() {
319            return null;
320        }
321
322        @Override
323        public OutputStream getOutputStream() {
324            return null;
325        }
326
327        @Override
328        public boolean isConnected() {
329            return mConnected;
330        }
331
332    }
333
334
335    private class TestSocketCreator implements MailTransport.SocketCreator {
336
337        @Override
338        public Socket createSocket() throws MessagingException {
339            Socket socket = new TestSocket();
340            return socket;
341        }
342
343    }
344
345    private ImapHelper createMockImapHelper() {
346        return mock(ImapHelper.class);
347    }
348
349    /**
350     * @return a mock Network that can create a TestSocket with {@code getSocketFactory()
351     * .createSocket()}
352     */
353    private Network createMockNetwork() {
354        Network network = mock(Network.class);
355        SocketFactory mockSocketFactory = mock(SocketFactory.class);
356        try {
357            when(mockSocketFactory.createSocket()).thenReturn(new TestSocket());
358        } catch (IOException e) {
359            //ignored
360        }
361        when(network.getSocketFactory()).thenReturn(mockSocketFactory);
362        return network;
363    }
364
365    /**
366     * @return a mock Network like {@link MailTransportTest#createMockNetwork()}, but also supports
367     * {@link Network#getAllByName(String)} with one valid result.
368     */
369    private Network createOneIpMockNetwork() {
370        Network network = createMockNetwork();
371        try {
372            when(network.getAllByName(HOST_ADDRESS))
373                    .thenReturn(new InetAddress[] {VALID_INET_ADDRESS});
374        } catch (UnknownHostException e) {
375            //ignored
376        }
377
378        return network;
379    }
380
381    /**
382     * @return a mock Network like {@link MailTransportTest#createMockNetwork()}, but also supports
383     * {@link Network#getAllByName(String)}, which will return 2 address with the first one
384     * invalid.
385     */
386    private Network createMultiIpMockNetwork() {
387        Network network = createMockNetwork();
388        try {
389            when(network.getAllByName(HOST_ADDRESS))
390                    .thenReturn(new InetAddress[] {INVALID_INET_ADDRESS, VALID_INET_ADDRESS});
391        } catch (UnknownHostException e) {
392            //ignored
393        }
394
395        return network;
396    }
397
398    /**
399     * helper method to translate{@code host} into a InetAddress.
400     *
401     * @param host IP address of the host. Domain name should not be used as this method should not
402     * access the internet.
403     */
404    private static InetAddress createInetAddress(String host) {
405        try {
406            return InetAddress.getByName(host);
407        } catch (UnknownHostException e) {
408            return null;
409        }
410    }
411
412
413}
414