/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.qpid.proton.systemtests.engine; import static java.util.EnumSet.of; import static org.apache.qpid.proton.engine.EndpointState.ACTIVE; import static org.apache.qpid.proton.engine.EndpointState.CLOSED; import static org.apache.qpid.proton.engine.EndpointState.UNINITIALIZED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.apache.qpid.proton.Proton; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.transport.Close; import org.apache.qpid.proton.amqp.transport.ErrorCondition; import org.apache.qpid.proton.amqp.transport.Open; import org.apache.qpid.proton.engine.Connection; import org.apache.qpid.proton.engine.Endpoint; import org.apache.qpid.proton.engine.EndpointState; import org.apache.qpid.proton.engine.Session; import org.apache.qpid.proton.engine.Transport; import org.apache.qpid.proton.engine.impl.AmqpFramer; import org.junit.Ignore; import org.junit.Test; /** * Implicitly tests both {@link Connection} and {@link Transport} (e.g. for stuff like the AMQP header exchange). * * TODO test that the connection properties, connection capability, and error info maps have keys that are exclusively of type Symbol. */ public class ConnectionTest { private static final String SERVER_CONTAINER = "serverContainer"; private static final String CLIENT_CONTAINER = "clientContainer"; private final Transport _clientTransport = Proton.transport(); private final Transport _serverTransport = Proton.transport(); private final TransportPumper _pumper = new TransportPumper(_clientTransport, _serverTransport); private final Connection _clientConnection = Proton.connection(); private final Connection _serverConnection = Proton.connection(); private final AmqpFramer _framer = new AmqpFramer(); // 2.4.1 Opening A Connection /** */ @Test public void testOpenConnection() { _pumper.pumpAll(); bindAndOpenConnections(); } /** Container id is a mandatory field so this should cause an error */ @Test public void testReceiptOfOpenWithoutContainerId_causesTODO() { _pumper.pumpAll(); Open openWithoutContainerId = new Open(); byte[] openFrameBuffer = _framer.generateFrame(0, openWithoutContainerId); int serverConsumed = _serverTransport.input(openFrameBuffer, 0, openFrameBuffer.length); assertEquals(openFrameBuffer.length, serverConsumed); assertEquals(_serverTransport.capacity(), Transport.END_OF_STREAM); } /** * "Prior to any explicit negotiation, the maximum frame size is 512 (MIN-MAX-FRAME-SIZE) and the maximum channel number is 0" * */ @Test public void testReceiptOfOpenExactlyDefaultMaximumFrameSize() { _pumper.pumpAll(); _serverTransport.bind(_serverConnection); assertEnpointState(_serverConnection, UNINITIALIZED, UNINITIALIZED); // containerId and extended header sized to give an open frame // exactly 512 bytes in length. String containerId = "12345678"; int extendedHeaderSize = 122 * 4; Open open = new Open(); open.setContainerId(containerId); byte[] openFrameBuffer = _framer.generateFrame(0, new byte[extendedHeaderSize], open); assertEquals("Test requires a frame of size MIN_MAX_FRAME_SIZE", Transport.MIN_MAX_FRAME_SIZE, openFrameBuffer.length); int serverConsumed = _serverTransport.input(openFrameBuffer, 0, openFrameBuffer.length); assertEquals(openFrameBuffer.length, serverConsumed); // Verify that the server has seen the Open arrive assertEnpointState(_serverConnection, UNINITIALIZED, ACTIVE); assertEquals(containerId, _serverConnection.getRemoteContainer()); } /** * "Prior to any explicit negotiation, the maximum frame size is 512 (MIN-MAX-FRAME-SIZE) and the maximum channel number is 0" */ @Test public void testReceiptOfOpenBiggerThanDefaultMaximumFrameSize_causesTODO() { _pumper.pumpAll(); _serverTransport.bind(_serverConnection); assertEnpointState(_serverConnection, UNINITIALIZED, UNINITIALIZED); // containerId and extended header sized to give an open frame // 1 byte larger the than 512 bytes permitted before negotiation by the AMQP spec. String containerId = "123456789"; int extendedHeaderSize = 122 * 4; Open bigOpen = new Open(); bigOpen.setContainerId(containerId); byte[] openFrameBuffer = _framer.generateFrame(0, new byte[extendedHeaderSize], bigOpen); assertEquals("Test requires a frame of size MIN_MAX_FRAME_SIZE + 1", Transport.MIN_MAX_FRAME_SIZE + 1, openFrameBuffer.length); int serverConsumed = _serverTransport.input(openFrameBuffer, 0, openFrameBuffer.length); assertEquals(openFrameBuffer.length, serverConsumed); // TODO server should indicate error but currently both implementations currently process // the larger frames. The following assertions should fail but currently pass. assertEnpointState(_serverConnection, UNINITIALIZED, ACTIVE); assertNotNull(_serverConnection.getRemoteContainer()); } @Test public void testReceiptOfSecondOpen_causesTODO() { bindAndOpenConnections(); Open secondOpen = new Open(); // erroneous secondOpen.setContainerId("secondOpen"); byte[] openFrameBuffer = _framer.generateFrame(0, secondOpen); int serverConsumed = _serverTransport.input(openFrameBuffer, 0, openFrameBuffer.length); assertEquals(openFrameBuffer.length, serverConsumed); // TODO server should indicate error but currently both implementation currently // allow this condition } /** "each peer MUST send an open frame before sending any other frames" * * @see ConnectionTest#testReceiptOfCloseBeforeOpen_causesTODO() */ public void testReceiptOfIntialFrameOtherThanOpen_causesTODO() { } /** * 2.4.5 "Implementations MUST be prepared to handle empty frames arriving on any valid channel" * * TODO consider moving to {@link TransportTest} once we have a less Connection-centric way of * checking health than calling {@link #bindAndOpenConnections()} */ @Test public void testReceiptOfInitialEmptyFrame_isAllowed() { _pumper.pumpAll(); byte[] emptyFrame = _framer.createEmptyFrame(0); int bytesConsumed = _serverTransport.input(emptyFrame, 0, emptyFrame.length); assertEquals(emptyFrame.length, bytesConsumed); bindAndOpenConnections(); } /** "The open frame can only be sent on channel 0" */ @Test @Ignore("Reinstate once it is agreed how error condition will be reported to user of API") public void testReceiptOfOpenOnNonZeroChannelNumber_causesTODO() { _pumper.pumpAll(); Open open = new Open(); open.setContainerId(SERVER_CONTAINER); int nonZeroChannelId = 1; byte[] buf = _framer.generateFrame(nonZeroChannelId, open); int rv = _serverTransport.input(buf, 0, buf.length); // TODO server should indicate error } /** * "After sending the open frame and reading its partner's open frame a peer MUST operate within * mutually acceptable limitations from this point forward" * see 2.7.1 "A peer that receives an oversized frame MUST close the connection with the framing-error error-code" */ public void testReceiptOfFrameLargerThanAgreedMaximumSize_causesTODO() { } public void testThatSentFramesAreWithinMaximumSizeLimit() { } // 2.4.2 Pipelined Open /** test that the other peer accepts the pipelined frames and creates an open connection */ @Test public void testReceiptOfOpenUsingPipelining() { _clientConnection.setContainer(CLIENT_CONTAINER); _clientTransport.bind(_clientConnection); _clientConnection.open(); _serverTransport.bind(_serverConnection); // when pipelining, we delay pumping until the connection is both bound and opened _pumper.pumpOnceFromClientToServer(); assertEnpointState(_clientConnection, ACTIVE, UNINITIALIZED); assertEnpointState(_serverConnection, UNINITIALIZED, ACTIVE); } /** test that the other peer accepts the pipelined frames and creates an already-closed connection */ @Test public void testReceiptOfOpenThenCloseUsingPipelining() { _clientConnection.setContainer(CLIENT_CONTAINER); _clientTransport.bind(_clientConnection); _clientConnection.open(); _clientConnection.close(); _serverTransport.bind(_serverConnection); _pumper.pumpOnceFromClientToServer(); assertEnpointState(_clientConnection, CLOSED, UNINITIALIZED); assertEnpointState(_serverConnection, UNINITIALIZED, CLOSED); } /** * Similar to {@link #testReceiptOfOpenUsingPipelining()} but opens both ends of the connection * so we can actually use it. */ @Test public void testOpenConnectionUsingPipelining() { _clientConnection.setContainer(CLIENT_CONTAINER); _clientTransport.bind(_clientConnection); _clientConnection.open(); _serverConnection.setContainer(SERVER_CONTAINER); _serverTransport.bind(_serverConnection); _serverConnection.open(); _pumper.pumpAll(); assertEnpointState(_clientConnection, ACTIVE, ACTIVE); assertEnpointState(_serverConnection, ACTIVE, ACTIVE); assertConnectionIsUsable(); } // 2.4.3 Closing A Connection and 2.7.9 Close /** * "each peer MUST write a close frame" * Omits the optional error field */ @Test public void testCloseConnection() { bindAndOpenConnections(); assertEnpointState(_clientConnection, ACTIVE, ACTIVE); assertEnpointState(_serverConnection, ACTIVE, ACTIVE); _clientConnection.close(); assertEnpointState(_clientConnection, CLOSED, ACTIVE); assertEnpointState(_serverConnection, ACTIVE, ACTIVE); _pumper.pumpAll(); assertEnpointState(_clientConnection, CLOSED, ACTIVE); assertEnpointState(_serverConnection, ACTIVE, CLOSED); _serverConnection.close(); assertEnpointState(_clientConnection, CLOSED, ACTIVE); assertEnpointState(_serverConnection, CLOSED, CLOSED); _pumper.pumpAll(); assertEnpointState(_clientConnection, CLOSED, CLOSED); assertEnpointState(_serverConnection, CLOSED, CLOSED); } /** * "each peer MUST write a close frame with a code indicating the reason for closing" * Also see 2.8.16 Connection Error */ @Test @SuppressWarnings({ "rawtypes", "unchecked" }) public void testCloseConnectionWithErrorCode_causesCloseFrameContainingErrorCodeToBeSent() { bindAndOpenConnections(); /* * TODO javadoc for {@link Connection#getCondition()} states null is returned if there is no condition, * this differs from the implementation of both Proton-c and Proton-j. */ assertNull(_clientConnection.getCondition().getCondition()); assertNull(_serverConnection.getCondition().getCondition()); assertNull(_clientConnection.getRemoteCondition().getCondition()); assertNull(_serverConnection.getRemoteCondition().getCondition()); ErrorCondition clientErrorCondition = new ErrorCondition(Symbol.getSymbol("myerror"), "mydescription"); Map info = new HashMap(); info.put(Symbol.getSymbol("simplevalue"), "value"); info.put(Symbol.getSymbol("list"), Arrays.asList("e1", "e2", "e3")); clientErrorCondition.setInfo(info); _clientConnection.setCondition(clientErrorCondition); _clientConnection.close(); _pumper.pumpAll(); assertEquals(clientErrorCondition, _serverConnection.getRemoteCondition()); assertNull(_serverConnection.getCondition().getCondition()); } /** * "each peer MUST write a close frame with a code indicating the reason for closing" */ public void testReceiptOfConnectionCloseContainingErrorCode_allowsErrorCodeToBeObserved() { } /** * A test for when the connection close frame contains a session error * rather than a connection error. This is allowed by the spec. */ public void testReceiptOfConnectionCloseContainingNonConnectionErrorCode_causesTODO() { } /** "This frame MUST be the last thing ever written onto a connection." */ public void testUsingProtonAfterClosingConnection_doesntCauseFrameToBeSent() { } /** "This frame MUST be the last thing ever written onto a connection." */ public void testReceiptOfFrameAfterClose_causesTODO() { } /** "A close frame MAY be received on any channel up to the maximum channel number negotiated in open" */ public void testReceiptOfCloseOnNonZeroChannelNumber_causesHappyPathTODO() { } /** * "each peer MUST send an open frame before sending any other frames" */ @Test public void testReceiptOfCloseBeforeOpen_causesTODO() { _pumper.pumpAll(); Close surprisingClose = new Close(); byte[] buf = _framer.generateFrame(0, surprisingClose); _serverTransport.input(buf, 0, buf.length); // TODO server should indicate error } // 2.4.4 Simultaneous Close /** "both endpoints MAY simultaneously" */ public void testPeersCloseConnectionSimultaneously() { } // 2.4.5 Idle Timeout Of A Connection public void testReceiptOfFrame_preventsIdleTimeoutOccurring() { } /** "If the threshold is exceeded, then a peer SHOULD try to gracefully close the connection using a close frame with an error explaining why" */ public void testReceiptOfFrameTooLate_causedIdleTimeoutToOccur() { } /** "Each peer has its own (independent) idle timeout." */ public void testPeersWithDifferentIdleTimeouts_timeOutAtTheCorrectTimes() { } /** * "If the value is not set, then the sender does not have an idle time-out. However, * senders doing this SHOULD be aware that implementations MAY choose to use an internal default * to efficiently manage a peer's resources." */ public void testReceiptOfFrameWithZeroIdleTimeout_causesNoIdleFramesToBeSent() { } /** * "If a peer can not, for any reason support a proposed idle timeout, * then it SHOULD close the connection using a close frame with an error explaining why" */ public void testReceiptOfOpenWithUnsupportedTimeout_causesCloseWithError() { } /** * implementations ... MUST use channel 0 if a maximum channel number has not yet been negotiated * (i.e., before an open frame has been received) */ public void testReceiptOfEmptyFrameOnNonZeroChannelBeforeMaximumChannelsNegotiated_causesTODO() { } // 2.4.7 State transitions /** * The DISCARDING state is a variant of the CLOSE_SENT state where the close is triggered by an error. * In this case any incoming frames on the connection MUST be silently discarded until the peer's close frame is received */ public void testReceiptOfFrameWhenInDiscardingState_isIgnored() { } // 2.7.1 Open public void testReceiptOfOpen_containerCanBeRetrieved() { } /** * The spec says: * "If no hostname is provided the receiving peer SHOULD select a default based on its own configuration" * but Proton's Engine layer does not do any defaulting - this is the responsibility * of other layers e.g. Messenger or Driver. */ public void testReceiptOfOpenWithoutHostname_nullHostnameIsRetrieved() { } public void testReceiptOfOpenWithHostname_hostnameCanBeRetrieved() { } /** * "Both peers MUST accept frames of up to 512 (MIN-MAX-FRAME-SIZE) octets." */ public void testReceiptOfOpenWithMaximumFramesizeLowerThanMinMaxFrameSize_causesTODO() { } public void testInitiatingPeerAndReceivingPeerUseDifferentMaxFrameSizes() { } public void testReceiptOfSessionBeginThatBreaksChannelMax_causesTODO() { } public void testCreationOfSessionThatBreaksChannelMax_causesTODO() { } public void testOpenConnectionWithPeersUsingUnequalChannelMax_enforcesLowerOfTwoValues() { } public void testOpenConnectionWithOnePeerUsingUnsetChannelMax_enforcesTheSetValue() { } public void testReceiptOfBeginWithInUseChannelId_causesTODO() { } /** "If a session is locally initiated, the remote-channel MUST NOT be set." */ public void testReceiptOfUnsolicitedBeginWithChannelId_causesTODO() { } /** * "When an endpoint responds to a remotely initiated session, the remote-channel MUST be set * to the channel on which the remote session sent the begin." */ public void testThatBeginResponseContainsChannelId() { } /** * I imagine we will want to begin ChannelMax number of sessions, then end * a session from the 'middle'. Then check we are correctly begin a new * channel. */ public void testEnd_channelNumberAvailableForReuse() { } public void testReceiptOfOpenWithOutgoingLocales_outgoingLocalesCanBeRetrieved() { } /** "A null value or an empty list implies that only en-US is supported. " */ public void testReceiptOfOpenWithNullOutgoingLocales_defaultOutgoingLocaleCanBeRetrieved() { } /** "A null value or an empty list implies that only en-US is supported. " */ public void testReceiptOfOpenWithEmptyListOfOutgoingLocales_defaultOutgoingLocaleCanBeRetrieved() { } public void testReceiptOfOpenWithIncomingLocales_incomingLocalesCanBeRetrieved() { } /** "A null value or an empty list implies that only en-US is supported. " */ public void testReceiptOfOpenWithNullIncomingLocales_defaultIncomingLocaleCanBeRetrieved() { } /** "A null value or an empty list implies that only en-US is supported. " */ public void testReceiptOfOpenWithEmptyListOfIncomingLocales_defaultIncomingLocaleCanBeRetrieved() { } // TODO It seems that currently Proton-j merely exposes the remote capabilities to // the user and is seems to be a end-user responsibility to enforce "If the receiver of the // offered-capabilities requires an extension capability which is not present in the // offered-capability list then it MUST close the connection.". However, i wonder if this // is an omission -- surely Proton could valid that request desirable capabilities are // offered by the remote??? public void testReceiptOfOpenWithOfferedCapabilities_offeredCapabilitiesCanBeRetrieved() { } public void testReceiptOfOpenWithDesiredCapabilities_desiredCapabilitiesCanBeRetrieved() { } public void testReceiptOfOpenWithProperties_propertiesCanBeRetrieved() { } // Transport/Connection related api-inspired tests /** * TODO is there a limit on the number of connections? * Also try closing them in a different order to their creation. */ public void testCreateMultipleConnections() { } public void testBindTwoConnectionsToATransport_causesTODO() { } public void testBindAConnectionToTwoTransports_causesTODO() { } /** * TODO possibly try to bind this "opened" connection too if it doesn't go pop before this. */ public void testOpenBeforeBind_causesTODO() { } public void testOpenTwice_throwsExceptionTODO() { } public void testOpenAfterClose_throwsExceptionTODO() { } // Connection.java-related api-inspired tests /** * also test that the session appears in the connection's session list */ public void testCreateSession() { } public void testSessionHeadWhenNoSessionsExist_returnsNull() { } public void testSessionHead_returnsSessionsMatchingCriteria() { } public void testLinkHeadWhenNoLinksExist_returnsNull() { } public void testLinkHead_returnsLinksMatchingCriteria() { } public void testGetWorkHeadWhenNoWork_returnsNull() { } public void testGetWorkHeadWhenOneDeliveryIsPending_returnsTheDelivery() { } /** * use a name that is longer than the limit of AMQShortString */ public void testSetContainerWithLongName_isAllowed() { } public void testSetContainerWithNullName_throwsException() { } public void testSetContainerWithEmptyName_throwsException() { } public void testSetContainerAfterOpeningConnection_throwsExceptionTODO() { } public void testOpenWithoutContainerName_throwsExceptionTODO() { } public void testGetRemoteContainerBeforeOpen_returnsNull() { } public void testGetRemoteContainerBeforeReceiptOfOpen_returnsNull() { } public void testSetHostnameWithLongName_isAllowed() { } /** * Proton does not require the conventional foo.bar.com format for hostnames. */ public void testSetHostnameWithNonstandardName_isAllowed() { } public void testSetHostnameAfterOpeningConnection_throwsExceptionTODO() { } public void testSetOfferedCapabilitiesAfterOpeningConnection_throwsExceptionTODO() { } public void testSetDesiredCapabilitiesAfterOpeningConnection_throwsExceptionTODO() { } public void testSetPropertiesAfterOpeningConnection_throwsExceptionTODO() { } // Endpoint api-inspired tests public void testGetLocalStateBeforeOpen_returnsUninitialised() { } public void testGetLocalStateAfterClose_returnsClosed() { } public void testGetRemoteStateBeforeReceiptOfOpen_returnsUninitialised() { } public void testGetRemoteStateAfterReceiptOfClose_returnsClosed() { } public void testFree_isAllowed() { } public void testSetContext_contextCanBeRetrieved() { } public void testGetContextWithoutSettingContext_returnsNull() { } private void assertConnectionIsUsable() { Session clientSesion = _clientConnection.session(); clientSesion.open(); _pumper.pumpAll(); Session serverSession = _serverConnection.sessionHead(of(UNINITIALIZED), of(ACTIVE)); serverSession.open(); _pumper.pumpAll(); assertEnpointState(clientSesion, ACTIVE, ACTIVE); assertEnpointState(serverSession, ACTIVE, ACTIVE); } private void bindAndOpenConnections() { // TODO should we be checking local and remote error conditions as part of this? _clientConnection.setContainer(CLIENT_CONTAINER); _serverConnection.setContainer(SERVER_CONTAINER); assertEnpointState(_clientConnection, UNINITIALIZED, UNINITIALIZED); assertEnpointState(_serverConnection, UNINITIALIZED, UNINITIALIZED); _clientTransport.bind(_clientConnection); _serverTransport.bind(_serverConnection); _clientConnection.open(); assertEnpointState(_clientConnection, ACTIVE, UNINITIALIZED); assertEnpointState(_serverConnection, UNINITIALIZED, UNINITIALIZED); _pumper.pumpAll(); assertEnpointState(_clientConnection, ACTIVE, UNINITIALIZED); assertEnpointState(_serverConnection, UNINITIALIZED, ACTIVE); _serverConnection.open(); assertEnpointState(_clientConnection, ACTIVE, UNINITIALIZED); assertEnpointState(_serverConnection, ACTIVE, ACTIVE); _pumper.pumpAll(); assertEnpointState(_clientConnection, ACTIVE, ACTIVE); assertEnpointState(_serverConnection, ACTIVE, ACTIVE); } private void assertEnpointState(Endpoint endpoint, EndpointState localState, EndpointState remoteState) { assertEquals("Unexpected local state", localState, endpoint.getLocalState()); assertEquals("Unexpected remote state", remoteState, endpoint.getRemoteState()); } }