JsseSslStreamSinkConduitTestCase.java

/*
 * JBoss, Home of Professional Open Source.
 *
 * Copyright 2013 Red Hat, Inc. and/or its affiliates, and individual
 * contributors as indicated by the @author tags.
 *
 * 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 org.xnio.ssl;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.xnio.ssl.mock.SSLEngineMock.CLOSE_MSG;
import static org.xnio.ssl.mock.SSLEngineMock.HANDSHAKE_MSG;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.FINISH;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_FAULTY_TASK;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_TASK;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_UNWRAP;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_WRAP;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.PERFORM_REQUESTED_ACTION;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;

import javax.net.ssl.SSLEngineResult.HandshakeStatus;

import org.jmock.integration.junit4.JUnitRuleMockery;
import org.junit.Rule;
import org.junit.Test;
import org.xnio.ssl.mock.SSLEngineMock;


/**
 * Test write operations on {@link JsseSslStreamSinkConduit}.
 * 
 * @author <a href="mailto:frainone@redhat.com">Flavia Rainone</a>
 */
public class JsseSslStreamSinkConduitTestCase extends AbstractSslConnectionTest {
    @Rule
    public final JUnitRuleMockery context = new JUnitRuleMockery();
    @Test
    public void writeWithoutHandshake() throws IOException {
        // no handshake actions for engineMock this time, meaning that it will just wrap and unwrap without any handshake
        // the message we want to write
        final ByteBuffer buffer = ByteBuffer.allocate(100);
        buffer.put("MockTest".getBytes("UTF-8")).flip();
        // attempt to write... conduit is expected to write the entire message without any issues
        assertEquals(8, sinkConduit.write(buffer));
        assertFalse(buffer.hasRemaining());
        // conduit should not be able to shutdown writes... for that, it must receive a close message
        sinkConduit.terminateWrites();
        assertFalse(sinkConduit.flush());
        // send the close message
        conduitMock.setReadData(CLOSE_MSG);
        conduitMock.enableReads(true);
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());

        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage("MockTest", CLOSE_MSG);
    }

    @Test
    public void writeWithoutHandshakeMappedWrap() throws IOException {
        // map the wrap
        engineMock.addWrapEntry("MockTest", "WRAPPED_MOCK_TEST");

        // no handshake actions for engineMock this time, meaning that it will just wrap and unwrap without any handshake
        // the message we want to write
        final ByteBuffer buffer = ByteBuffer.allocate(100);
        buffer.put("MockTest".getBytes("UTF-8")).flip();
        // attempt to write... conduit is expected to write the entire message without any issues
        assertEquals(8, sinkConduit.write(buffer));
        assertFalse(buffer.hasRemaining());
        // conduit should not be able to shutdown writes... for that, it must receive a close message
        sinkConduit.terminateWrites();
        assertFalse(sinkConduit.flush());
        // send the close message
        conduitMock.setReadData(CLOSE_MSG);
        conduitMock.enableReads(true);
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());

        // close connection 
        sourceConduit.terminateReads();
        // data expected to have bee written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage("WRAPPED_MOCK_TEST", CLOSE_MSG);
    }

    @Test
    public void writeWithSimpleHandshake() throws IOException {
        // map all data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "handshake");
        engineMock.addWrapEntry("MockTest", "mock test works!");
        engineMock.addWrapEntry(CLOSE_MSG, "channel closed");
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH);
        // set ReadData on conduitMock
        conduitMock.setReadData("handshake", "channel closed");
        conduitMock.enableFlush(false);
        // message we want to write
        final ByteBuffer buffer = ByteBuffer.allocate(100);
        final ByteBuffer[] buffers = new ByteBuffer[]{buffer};
        buffer.put("MockTest".getBytes("UTF-8")).flip();

        // attempt to write... conduit is expected to stop on NEED_WRAP, as flush is disabled on conduitMock
        assertEquals(0, sinkConduit.write(buffers, 0, 1));
        assertFalse(conduitMock.isFlushed());
        conduitMock.enableFlush(true);
        assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());
        // attempt to write... conduit is expected to stop on NEED_UNWRAP, as read on conduitMock is not available
        assertEquals(0, sinkConduit.write(buffers, 0, 1));
        assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());
        assertTrue(conduitMock.isFlushed());

        // enable read, now read data will be available to the source conduit
        conduitMock.enableReads(true);
        // conduit is expected to write all data from buffer
        assertEquals(8, sinkConduit.write(new ByteBuffer[]{buffer, ByteBuffer.allocate(0)}, 0, 2));
        assertFalse(buffer.hasRemaining());

        // for coverage purposes, attempt to write empty buffers
        assertEquals(0, sinkConduit.write(new ByteBuffer[]{buffer, ByteBuffer.allocate(0)}, 0, 2));

        // conduit should be able to terminate writes
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage("handshake", "mock test works!", "channel closed");
    }

    @Test
    public void writeWithTasklessHandshake() throws IOException {
        // map data to be read and written
        engineMock.addWrapEntry("MockTest", "{testWriteWithTasklessHandshake}");
        engineMock.addWrapEntry(CLOSE_MSG, " _ ");

        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, FINISH);
        // set ReadData on conduitMock
        conduitMock.setReadData(SSLEngineMock.HANDSHAKE_MSG, " _ ");
        // message we want to write
        final ByteBuffer buffer = ByteBuffer.allocate(100);
        buffer.put("MockTest".getBytes("UTF-8")).flip();

        // attempt to write... conduit is expected to stop on NEED_UNWRAP, as read on conduitMock is not available
        assertEquals(0, sinkConduit.write(buffer));
        assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());

        // enable read, now read data will be available to the source conduit
        conduitMock.enableReads(true);
        // conduit is expected to write all data from buffer
        assertEquals(8, sinkConduit.write(buffer));
        assertFalse(buffer.hasRemaining());

        // conduit should be able to shutdown writes
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage(HANDSHAKE_MSG, "{testWriteWithTasklessHandshake}", " _ ");
    }

    @Test
    public void multipleWritesWithSimpleHandshake() throws IOException {
        // map data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "{handshake data}");
        engineMock.addWrapEntry("MockTest", "{data}");
        engineMock.addWrapEntry(CLOSE_MSG, "{message closed}");
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH);
        // set ReadData on conduitMock
        conduitMock.setReadData("{handshake data}");
        // enable read on conduitMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);
        // message we want to write
        final ByteBuffer buffer = ByteBuffer.allocate(100);
        buffer.put("MockTest".getBytes("UTF-8")).flip();

        // attempt to write... conduit is expected to write all messages without any issues
        for (int i = 0; i < 10; i++) {
            while(buffer.hasRemaining()) {
                assertEquals(8, sinkConduit.write(buffer));
                assertFalse(buffer.hasRemaining());
            }
            buffer.flip();
        }

        // conduit should not be able to terminate writes... for that, it must receive a close message
        sinkConduit.terminateWrites();
        assertFalse(sinkConduit.flush());
        // send the close message
        conduitMock.setReadData("{message closed}");
        conduitMock.enableReads(true);
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());

        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage("{handshake data}", "{data}", "{data}", "{data}", "{data}", "{data}", "{data}", "{data}",
                "{data}", "{data}", "{data}", "{message closed}");
    }

    @Test
    public void writeWithConnectionWithoutReadData() throws IOException {
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH);
        // message we want to write
        final ByteBuffer buffer = ByteBuffer.allocate(100);
        buffer.put("MockTest".getBytes("UTF-8")).flip();
        // attempt to write several times... conduit is expected to stop on NEED_UNWRAP, as read on conduitMock is disabled
        assertEquals(0, sinkConduit.write(buffer));
        assertEquals(0, sinkConduit.write(buffer));
        assertEquals(0, sinkConduit.write(buffer));
        // make sure that conduit managed to do the WRAP and got stalled on NEED_UNWRAP handshake status
        assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());
        // conduit should not be able to shutdown writes... for that, it must receive a close message
        sinkConduit.terminateWrites();
        assertFalse(sinkConduit.flush());
        conduitMock.setReadData(HANDSHAKE_MSG);
        conduitMock.enableReads(true);
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage(HANDSHAKE_MSG, CLOSE_MSG);
    }

    @Test
    public void writeWithConstantHandshake() throws IOException {
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, NEED_WRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, NEED_WRAP, NEED_WRAP, NEED_UNWRAP, NEED_TASK, NEED_TASK, NEED_TASK, NEED_TASK, FINISH,
                PERFORM_REQUESTED_ACTION, NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION);
        // set ReadData on conduitMock
        conduitMock.setReadData(HANDSHAKE_MSG, HANDSHAKE_MSG, HANDSHAKE_MSG, HANDSHAKE_MSG, CLOSE_MSG);
        // enable read on conduitMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);
        // messages we plan to write
        final ByteBuffer buffer1 = ByteBuffer.allocate(10);
        buffer1.put("MockTest1".getBytes("UTF-8")).flip();
        final ByteBuffer buffer2 = ByteBuffer.allocate(10);
        buffer2.put("MockTest2".getBytes("UTF-8")).flip();
        final ByteBuffer buffer3 = ByteBuffer.allocate(10);
        buffer3.put("MockTest3".getBytes("UTF-8")).flip();
        final ByteBuffer buffer4 = ByteBuffer.allocate(10);
        buffer4.put("MockTest4".getBytes("UTF-8")).flip();
        // attempt to write... conduit is expected to write all messages without any issues despite the constant handshaking action
        assertEquals(9, sinkConduit.write(buffer1));
        assertFalse(buffer1.hasRemaining());
        assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus()); // next handshake action is NEED_UNWRAP
        assertEquals(9, sinkConduit.write(buffer2));
        assertFalse(buffer2.hasRemaining());
        assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus()); // next handshake action is NEED_WRAP
        assertEquals(9, sinkConduit.write(buffer3));
        assertFalse(buffer3.hasRemaining());
        assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus()); // next handshake action is NEED_WRAP
        assertEquals(9, sinkConduit.write(buffer4));
        assertFalse(buffer4.hasRemaining());
        // make sure that conduit managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());
        // conduit should be able to shutdown writes
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage(HANDSHAKE_MSG, "MockTest1", HANDSHAKE_MSG, "MockTest2", HANDSHAKE_MSG, HANDSHAKE_MSG,
                HANDSHAKE_MSG, "MockTest3", HANDSHAKE_MSG, "MockTest4", CLOSE_MSG);
    }

    @Test
    public void writeWithConstantHandshakeAndMappedData() throws IOException {
        // map data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "HANDSHAKE_MSG");
        engineMock.addWrapEntry("MockTest1", "MOCK 1");
        engineMock.addWrapEntry("MockTest2", "MOCK 2");
        engineMock.addWrapEntry("MockTest3", "MOCK 3");
        engineMock.addWrapEntry("MockTest4", "MOCK 4");
        engineMock.addWrapEntry(CLOSE_MSG, "CLOSE_MSG");
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, NEED_WRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, NEED_WRAP, NEED_WRAP, NEED_UNWRAP, NEED_TASK, NEED_TASK, NEED_TASK, NEED_TASK, FINISH,
                PERFORM_REQUESTED_ACTION, NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION);
        // set ReadData on conduitMock
        conduitMock.setReadData("HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG", "HANDSHAKE_MSG", "CLOSE_MSG");
        // enable read on conduitMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);
        // messages we plan to write
        final ByteBuffer buffer1 = ByteBuffer.allocate(10);
        buffer1.put("MockTest1".getBytes("UTF-8")).flip();
        final ByteBuffer buffer2 = ByteBuffer.allocate(10);
        buffer2.put("MockTest2".getBytes("UTF-8")).flip();
        final ByteBuffer buffer3 = ByteBuffer.allocate(10);
        buffer3.put("MockTest3".getBytes("UTF-8")).flip();
        final ByteBuffer buffer4 = ByteBuffer.allocate(10);
        buffer4.put("MockTest4".getBytes("UTF-8")).flip();
        // attempt to write... conduit is expected to write all messages without any issues despite the constant handshaking action
        assertEquals(9, sinkConduit.write(buffer1));
        assertFalse(buffer1.hasRemaining());
        assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus()); // next handshake action is NEED_UNWRAP
        assertEquals(9, sinkConduit.write(buffer2));
        assertFalse(buffer2.hasRemaining());
        assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus()); // next handshake action is NEED_WRAP
        assertEquals(9, sinkConduit.write(buffer3));
        assertFalse(buffer3.hasRemaining());
        assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus()); // next handshake action is NEED_WRAP
        assertEquals(9, sinkConduit.write(buffer4));
        assertFalse(buffer4.hasRemaining());
        // make sure that conduit managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());
        // conduit should be able to terminate writes
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage("HANDSHAKE_MSG", "MOCK 1", "HANDSHAKE_MSG", "MOCK 2", "HANDSHAKE_MSG", "HANDSHAKE_MSG",
                "HANDSHAKE_MSG", "MOCK 3", "HANDSHAKE_MSG", "MOCK 4", "CLOSE_MSG");
    }

    @Test
    public void writeWithIntercalatedHandshake() throws IOException {
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, PERFORM_REQUESTED_ACTION, NEED_WRAP, PERFORM_REQUESTED_ACTION, NEED_WRAP, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, PERFORM_REQUESTED_ACTION, PERFORM_REQUESTED_ACTION, NEED_UNWRAP, PERFORM_REQUESTED_ACTION,
                NEED_TASK, PERFORM_REQUESTED_ACTION, PERFORM_REQUESTED_ACTION, FINISH);
        // set ReadData on conduitMock
        conduitMock.setReadData(HANDSHAKE_MSG, HANDSHAKE_MSG, HANDSHAKE_MSG, CLOSE_MSG);
        // enable read on conduitMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);
        // message we plan to write
        final ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("write this".getBytes("UTF-8")).flip();
        // attempt to write... conduit is expected to write all messages without any issues despite the constant handshaking action
        for (int i = 0; i < 10; i++) {
            assertEquals("Failed at attempt to write number " + i, 10, sinkConduit.write(buffer));
            assertFalse(buffer.hasRemaining());
            buffer.flip();
        }
        // make sure that conduit managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());
        // conduit should be able to terminate writes
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage(HANDSHAKE_MSG, "write this", HANDSHAKE_MSG, "write this",
                HANDSHAKE_MSG, "write this", HANDSHAKE_MSG, "write this", "write this", "write this", "write this",
                "write this", "write this", "write this", CLOSE_MSG);
    }

    @Test
    public void writeWithIntercalatedHandshakeAndMappedData() throws IOException {
        // map all data to be read and written
        engineMock.addWrapEntry(HANDSHAKE_MSG, "[!@#$%^&*()_]");
        engineMock.addWrapEntry("write this", "this");
        engineMock.addWrapEntry(CLOSE_MSG, "[_)(*&^%$#@!]");

        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_WRAP, NEED_UNWRAP, NEED_TASK, FINISH, PERFORM_REQUESTED_ACTION,
                NEED_WRAP, PERFORM_REQUESTED_ACTION, NEED_WRAP, PERFORM_REQUESTED_ACTION, NEED_WRAP, PERFORM_REQUESTED_ACTION,
                NEED_UNWRAP, PERFORM_REQUESTED_ACTION, PERFORM_REQUESTED_ACTION, NEED_UNWRAP, PERFORM_REQUESTED_ACTION,
                NEED_TASK, PERFORM_REQUESTED_ACTION, PERFORM_REQUESTED_ACTION, FINISH);
        // set ReadData on conduitMock
        conduitMock.setReadData("[!@#$%^&*()_]", "[!@#$%^&*()_]", "[!@#$%^&*()_]", "[_)(*&^%$#@!]");
        // enable read on conduitMock, meaning that data above will be available to be read right away
        conduitMock.enableReads(true);
        // message we plan to write
        final ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("write this".getBytes("UTF-8")).flip();
        // attempt to write... conduit is expected to write all messages without any issues despite the constant handshaking action
        for (int i = 0; i < 10; i++) {
            assertEquals("Failed at attempt to write number " + i, 10, sinkConduit.write(buffer));
            assertFalse(buffer.hasRemaining());
            buffer.flip();
        }
        // make sure that conduit managed to do the WRAP and there is no more handshake actions left
        assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());
        // conduit should be able to terminate writes
        sinkConduit.terminateWrites();
        assertTrue(sinkConduit.flush());
        // close connection
        sourceConduit.terminateReads();
        // data expected to have been written to 'conduitMock' by 'sinkConduit'
        assertWrittenMessage("[!@#$%^&*()_]", "this", "[!@#$%^&*()_]", "this", "[!@#$%^&*()_]", "this", "[!@#$%^&*()_]",
                "this", "this", "this", "this", "this", "this", "this", "[_)(*&^%$#@!]");
    }

    @Test
    public void attemptToWriteWithFaultyTask() throws IOException {
        // set the handshake actions that engineMock will emulate
        engineMock.setHandshakeActions(NEED_FAULTY_TASK);
        // message we plan to write
        final ByteBuffer buffer = ByteBuffer.allocate(21);
        buffer.put("write this if you can".getBytes("UTF-8")).flip();
        // try to write a bunch of times, we will get an IOException at all of the times
        for (int i = 0; i < 10; i ++) {
            boolean failed = false;
            try {
                sinkConduit.write(buffer);
            } catch (IOException expected) {
                failed = true;
            }
            assertTrue(failed);
        }
    }

    @Test
    public void closeWithoutFlushing() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("abc".getBytes("UTF-8")).flip();
        sinkConduit.write(buffer);
        sourceConduit.terminateReads();
        assertWrittenMessage("abc", CLOSE_MSG);
    }
}