/* * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file 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. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MockAuthDelegate.h" #include "MockHTTP2Connection.h" #include "MockHTTP2Request.h" #include "MockMessageConsumer.h" #include "MockPostConnect.h" #include "MockPostConnectFactory.h" #include "MockTransportObserver.h" namespace alexaClientSDK { namespace acl { namespace transport { namespace test { using namespace acl::test; using namespace avsCommon::avs; using namespace avsCommon::avs::attachment; using namespace avsCommon::sdkInterfaces; using namespace avsCommon::utils; using namespace avsCommon::utils::http2; using namespace avsCommon::utils::http2::test; using namespace ::testing; /// Test endpoint. static const std::string TEST_AVS_ENDPOINT_STRING = "http://avs-alexa-na.amazon.com"; /// Expected Downchannel URL sent on requests. static const std::string AVS_DOWNCHANNEL_URL_PATH_EXTENSION = "/v20160207/directives"; /// Expected ping URL sent on requests. static const std::string AVS_PING_URL_PATH_EXTENSION = "/ping"; /// Expected Full Downchannel URL sent on requests. static const std::string FULL_DOWNCHANNEL_URL = TEST_AVS_ENDPOINT_STRING + AVS_DOWNCHANNEL_URL_PATH_EXTENSION; /// Expected Full ping URL sent on requests. static const std::string FULL_PING_URL = TEST_AVS_ENDPOINT_STRING + AVS_PING_URL_PATH_EXTENSION; /// A 100 millisecond delay used in tests. static const auto ONE_HUNDRED_MILLISECOND_DELAY = std::chrono::milliseconds(100); /// A 10 millisecond delay used in tests. static const auto TEN_MILLISECOND_DELAY = std::chrono::milliseconds(10); /// A short delay used in tests. static const auto SHORT_DELAY = std::chrono::seconds(1); /// Typical Timeout used in waiting for responses. static const auto RESPONSE_TIMEOUT = std::chrono::seconds(5); /// A longer timeout used in waiting for responses. static const auto LONG_RESPONSE_TIMEOUT = std::chrono::seconds(10); /// HTTP Authorization header. static const std::string HTTP_AUTHORIZATION_HEADER_BEARER = "Authorization: Bearer"; /// Authorization Token. static const std::string CBL_AUTHORIZATION_TOKEN = "AUTH_TOKEN"; /// A test AttachmentId string. static const std::string TEST_ATTACHMENT_ID_STRING_ONE = "testAttachmentId_1"; // Test message string to be sent. static const std::string TEST_MESSAGE = "aaabbccc"; // Test attachment string. static const std::string TEST_ATTACHMENT_MESSAGE = "MY_A_T_T_ACHMENT"; // Test attachment field. static const std::string TEST_ATTACHMENT_FIELD = "ATTACHMENT"; // A constant that means no call as a response static const long NO_CALL = -2; // Non-MIME payload static const std::string NON_MIME_PAYLOAD = "A_NON_MIME_PAYLOAD"; // A test directive. static const std::string DIRECTIVE1 = "{\"namespace:\"SpeechSynthesizer\",name:\"Speak\",messageId:\"351df0ff-8041-4891-a925-136f52d54da1\"," "dialogRequestId:\"58352bb2-7d07-4ba2-944b-10e6df25d193\"}"; // Another test directive. static const std::string DIRECTIVE2 = "{\"namespace:\"Alerts\",name:\"SetAlert\",messageId:\"ccc005b8-ca8f-4c34-aeb5-73a8dbbd8d37\",dialogRequestId:" "\"dca0bece-16a7-44f3-b940-e6c4ecc2b1f5\"}"; // Test MIME Boundary. static const std::string MIME_BOUNDARY = "thisisaboundary"; // MIME encoded DIRECTIVE1 with start and end MIME boundaries. static const std::string MIME_BODY_DIRECTIVE1 = "--" + MIME_BOUNDARY + "\r\nContent-Type: application/json" + "\r\n\r\n" + DIRECTIVE1 + "\r\n--" + MIME_BOUNDARY + "--\r\n"; ; // MIME encoded DIRECTIVE2 with start MIME boundary but no end MIME boundary. static const std::string MIME_BODY_DIRECTIVE2 = "--" + MIME_BOUNDARY + "\r\nContent-Type: application/json" + "\r\n\r\n" + DIRECTIVE2 + "\r\n--" + MIME_BOUNDARY + "\r\n"; // HTTP header to specify MIME boundary and content type. static const std::string HTTP_BOUNDARY_HEADER = "Content-Type: multipart/related; boundary=" + MIME_BOUNDARY + "; type=application/json"; // The maximum dedicated number of ping streams in HTTP2Transport static const unsigned MAX_PING_STREAMS = 1; // The maximum dedicated number of downchannel streams in HTTP2Transport static const unsigned MAX_DOWNCHANNEL_STREAMS = 1; // The maximum number of HTTP2 requests that can be enqueued at a time waiting for response completion static const unsigned MAX_AVS_STREAMS = 10; // Maximum allowed of POST streams static const unsigned MAX_POST_STREAMS = MAX_AVS_STREAMS - MAX_DOWNCHANNEL_STREAMS - MAX_PING_STREAMS; /// Test harness for @c HTTP2Transport class. class HTTP2TransportTest : public Test { public: /// Initial setup for tests. void SetUp() override; /// Cleanup method. void TearDown() override; protected: /** * Setup the handlers for the mocked methods @c AuthDelegateInterface::addAuthObserver(), @c * PostConnectFactoryInterface::createPostConnect() , @c PostConnectInterface::doPostConnect() , @c * TransportObserverInterface::onConnected(). * * @param sendOnPostConnected A boolean to specify whether to send onPostConnected() event when @c * PostConnectInterface::doPostConnect() is called. * @param expectConnected Specify that a call to TransportObserverInterface::onConnected() is expected. */ void setupHandlers(bool sendOnPostConnected, bool expectConnected); /** * Helper function to send @c Refreshed Auth State to the @c HTTP2Transport observer. * It also checks that a proper Auth observer has been registered by @c HTTP2Transport. */ void sendAuthStateRefreshed(); /** * Helper function to put the @c HTTP2Transport into connected state. */ void authorizeAndConnect(); /// The HTTP2Transport instance to be tested. std::shared_ptr m_http2Transport; /// The mock @c AuthDelegateInterface. std::shared_ptr m_mockAuthDelegate; /// The mock @c HTTP2ConnectionInterface. std::shared_ptr m_mockHttp2Connection; /// The mock @c MessageConsumerInterface. std::shared_ptr m_mockMessageConsumer; /// An instance of the @c AttachmentManager. std::shared_ptr m_attachmentManager; /// The mock @c TransportObserverInterface. std::shared_ptr m_mockTransportObserver; /// The mock @c PostConnectFactoryInterface std::shared_ptr m_mockPostConnectFactory; /// The mock @c PostConnectInterface. std::shared_ptr m_mockPostConnect; /// A promise that the Auth Observer will be set. PromiseFuturePair> m_authObserverSet; /// A promise that @c PostConnectFactoryInterface::createPostConnect() will be called). PromiseFuturePair m_createPostConnectCalled; /// A promise that @c PostConnectInterface:doPostConnect() will be called). PromiseFuturePair> m_doPostConnected; /// A promise that the @c TransportObserver.onConnected() will be called. PromiseFuturePair m_transportConnected; }; /** * A @c MessageRequestObserverInterface implementation used in this test. */ class TestMessageRequestObserver : public avsCommon::sdkInterfaces::MessageRequestObserverInterface { public: /* * Called when a message request has been processed by AVS. */ void onSendCompleted(MessageRequestObserverInterface::Status status) { m_status.setValue(status); } /* * Called when an exception is thrown when trying to send a message to AVS. */ void onExceptionReceived(const std::string& exceptionMessage) { m_exception.setValue(exceptionMessage); } /// A promise that @c MessageRequestObserverInterface::onSendCompleted() will be called with a @c /// MessageRequestObserverInterface::Status value PromiseFuturePair m_status; /// A promise that @c MessageRequestObserverInterface::onExceptionReceived() will be called with an exception /// message PromiseFuturePair m_exception; }; void HTTP2TransportTest::SetUp() { m_mockAuthDelegate = std::make_shared>(); m_mockHttp2Connection = std::make_shared>(FULL_DOWNCHANNEL_URL, FULL_PING_URL); m_mockMessageConsumer = std::make_shared>(); m_attachmentManager = std::make_shared(AttachmentManager::AttachmentType::IN_PROCESS); m_mockTransportObserver = std::make_shared>(); m_mockPostConnectFactory = std::make_shared>(); m_mockPostConnect = std::make_shared>(); m_mockAuthDelegate->setAuthToken(CBL_AUTHORIZATION_TOKEN); m_http2Transport = HTTP2Transport::create( m_mockAuthDelegate, TEST_AVS_ENDPOINT_STRING, m_mockHttp2Connection, m_mockMessageConsumer, m_attachmentManager, m_mockTransportObserver, m_mockPostConnectFactory); ASSERT_NE(m_http2Transport, nullptr); } void HTTP2TransportTest::TearDown() { m_http2Transport->shutdown(); } void HTTP2TransportTest::setupHandlers(bool sendOnPostConnected, bool expectConnected) { Sequence s1, s2; // Enforced ordering of mock method calls: // addAuthObserver should be before onConnected // createPostConnect should be before doPostConnect // Handle AuthDelegateInterface::addAuthObserver() when called. EXPECT_CALL(*m_mockAuthDelegate, addAuthObserver(_)) .InSequence(s1) .WillOnce(Invoke([this](std::shared_ptr argAuthObserver) { m_authObserverSet.setValue(argAuthObserver); })); { InSequence dummy; // Handle PostConnectFactoryInterface::createPostConnect() when called. EXPECT_CALL(*m_mockPostConnectFactory, createPostConnect()).WillOnce(InvokeWithoutArgs([this] { m_createPostConnectCalled.setValue(); return m_mockPostConnect; })); // Handle PostConnectInterface::doPostConnect() when called. EXPECT_CALL(*m_mockPostConnect, doPostConnect(_)) .InSequence(s2) .WillOnce(Invoke([this, sendOnPostConnected](std::shared_ptr transport) { m_doPostConnected.setValue(transport); if (sendOnPostConnected) { transport->onPostConnected(); } return true; }) ); } if (expectConnected) { // Handle TransportObserverInterface::onConnected() when called. EXPECT_CALL(*m_mockTransportObserver, onConnected(_)) .InSequence(s1, s2) .WillOnce(Invoke([this](std::shared_ptr transport) { m_transportConnected.setValue(); }) ); } } void HTTP2TransportTest::sendAuthStateRefreshed() { std::shared_ptr authObserver; // Wait for HTTP2Transport Auth server registration. ASSERT_TRUE(m_authObserverSet.waitFor(RESPONSE_TIMEOUT)); authObserver = m_authObserverSet.getValue(); // Check HTTP2Transport registered itself as the authObserver. ASSERT_TRUE(authObserver != nullptr); ASSERT_EQ(authObserver.get(), m_http2Transport.get()); // Send REFRESHED Auth State to HTTP2Transport. authObserver->onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::SUCCESS); } void HTTP2TransportTest::authorizeAndConnect() { setupHandlers(true, true); // Call connect(). m_http2Transport->connect(); // Deliver a 'REFRESHED' status to observers of AuthDelegateInterface. sendAuthStateRefreshed(); // The Mock HTTP2Request replies to any downchannel request with a 200. ASSERT_TRUE(m_mockHttp2Connection->respondToDownchannelRequests( static_cast(HTTPResponseCode::SUCCESS_OK), false, RESPONSE_TIMEOUT)); // Wait for PostConnectInterface:doPostConnect() call. ASSERT_TRUE(m_doPostConnected.waitFor(RESPONSE_TIMEOUT)); // Wait for a long time (up to 5 second), terminating the wait when the mock of // TransportObserverInterface::onConnected() is called. ASSERT_TRUE(m_transportConnected.waitFor(LONG_RESPONSE_TIMEOUT)); } /** * Test non-authorization on empty auth token. */ TEST_F(HTTP2TransportTest, emptyAuthToken) { // Send an empty Auth token. m_mockAuthDelegate->setAuthToken(""); setupHandlers(false, false); m_http2Transport->connect(); // Give m_http2Transport a chance to misbehave and send a request w/o authorization. std::this_thread::sleep_for(SHORT_DELAY); // Check that no HTTP requests created and sent at this point. ASSERT_TRUE(m_mockHttp2Connection->isRequestQueueEmpty()); sendAuthStateRefreshed(); // Should not send any HTTP2 Request. ASSERT_EQ(m_mockHttp2Connection->waitForRequest(ONE_HUNDRED_MILLISECOND_DELAY), nullptr); } /** * Test waiting for AuthDelegateInterface. */ TEST_F(HTTP2TransportTest, waitAuthDelegateInterface) { setupHandlers(false, false); m_http2Transport->connect(); // Give m_http2Transport a chance to misbehave and send a request w/o authorization. std::this_thread::sleep_for(SHORT_DELAY); // Check that no HTTP requests created and sent at this point. ASSERT_TRUE(m_mockHttp2Connection->isRequestQueueEmpty()); sendAuthStateRefreshed(); // Wait for HTTP2 Request. ASSERT_NE(m_mockHttp2Connection->waitForRequest(RESPONSE_TIMEOUT), nullptr); auto request = m_mockHttp2Connection->dequeRequest(); ASSERT_EQ(request->getUrl(), FULL_DOWNCHANNEL_URL); } /** * Test verifying the proper inclusion of bearer token in requests. */ TEST_F(HTTP2TransportTest, bearerTokenInRequest) { setupHandlers(false, false); m_mockHttp2Connection->setWaitRequestHeader(HTTP_AUTHORIZATION_HEADER_BEARER); m_http2Transport->connect(); sendAuthStateRefreshed(); // Wait for HTTP2 Request with Authorization: Bearer in header. ASSERT_TRUE(m_mockHttp2Connection->waitForRequestWithHeader(RESPONSE_TIMEOUT)); } /** * Test creation and triggering of post-connect object. */ TEST_F(HTTP2TransportTest, triggerPostConnectObject) { setupHandlers(false, false); // Don't expect TransportObserverInterface::onConnected() will be called. EXPECT_CALL(*m_mockTransportObserver, onConnected(_)).Times(0); m_http2Transport->connect(); sendAuthStateRefreshed(); // The Mock HTTP2Request replies to any downchannel request with 200. ASSERT_TRUE(m_mockHttp2Connection->respondToDownchannelRequests( static_cast(HTTPResponseCode::SUCCESS_OK), false, RESPONSE_TIMEOUT)); // Wait for a long time (up to 5 second), terminating the wait when the mock of // PostConnectFactoryInterface::createPostConnect() is called. ASSERT_TRUE(m_createPostConnectCalled.waitFor(RESPONSE_TIMEOUT)); // Wait for a long time (up to 5 second), terminating the wait when the mock of // PostConnectInterface::doPostConnect() is called. ASSERT_TRUE(m_doPostConnected.waitFor(RESPONSE_TIMEOUT)); } /** * Test delay of connection status until post-connect object created / notifies success. */ TEST_F(HTTP2TransportTest, connectionStatusOnPostConnect) { setupHandlers(true, true); // Call connect(). m_http2Transport->connect(); // Deliver a 'REFRESHED' status to observers of AuthDelegateInterface. sendAuthStateRefreshed(); // The Mock HTTP2Request replies to any downchannel request with a 200. ASSERT_TRUE(m_mockHttp2Connection->respondToDownchannelRequests( static_cast(HTTPResponseCode::SUCCESS_OK), false, RESPONSE_TIMEOUT)); // Wait for PostConnectInterface:doPostConnect() call. ASSERT_TRUE(m_doPostConnected.waitFor(RESPONSE_TIMEOUT)); // Wait for a long time (up to 5 second), terminating the wait when the mock of // TransportObserverInterface::onConnected() is called. ASSERT_TRUE(m_transportConnected.waitFor(LONG_RESPONSE_TIMEOUT)); } /** * Test retry upon failed downchannel connection. */ TEST_F(HTTP2TransportTest, retryOnDownchannelConnectionFailure) { setupHandlers(false, false); EXPECT_CALL(*m_mockTransportObserver, onConnected(_)).Times(0); // Call connect(). m_http2Transport->connect(); // Deliver a 'REFRESHED' status to observers of AuthDelegateInterface. sendAuthStateRefreshed(); // The Mock HTTP2Request replies to any downchannel request with a 500. ASSERT_TRUE(m_mockHttp2Connection->respondToDownchannelRequests( static_cast(SERVER_INTERNAL_ERROR), false, RESPONSE_TIMEOUT)); // Wait for a long time (up to 10 second), terminating the wait when the mock of HTTP2Connection receives a second // attempt to create a downchannel request. m_mockHttp2Connection->waitForRequest(LONG_RESPONSE_TIMEOUT, 2); } /** * Test sending of MessageRequest content. */ TEST_F(HTTP2TransportTest, messageRequestContent) { setupHandlers(false, false); // Call connect(). m_http2Transport->connect(); // Deliver a 'REFRESHED' status to observers of AuthDelegateInterface. sendAuthStateRefreshed(); ASSERT_TRUE(m_mockHttp2Connection->respondToDownchannelRequests( static_cast(HTTPResponseCode::SUCCESS_OK), false, RESPONSE_TIMEOUT)); // Wait for doPostConnect(). ASSERT_TRUE(m_doPostConnected.waitFor(RESPONSE_TIMEOUT)); // Send post connect message. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); m_http2Transport->sendPostConnectMessage(messageReq); // Wait for the postConnect message to become HTTP message request and HTTP body to be fully reassembled. auto postMessage = m_mockHttp2Connection->waitForPostRequest(LONG_RESPONSE_TIMEOUT); ASSERT_NE(postMessage, nullptr); // The number of MIME parts decoded should just be 1. ASSERT_EQ(postMessage->getMimeResponseSink()->getCountOfMimeParts(), 1u); // Get the first MIME part message. auto mimeMessage = postMessage->getMimeResponseSink()->getMimePart(0); std::string mimeMessageString(mimeMessage.begin(), mimeMessage.end()); // Check the MIME part is the message sent. ASSERT_EQ(TEST_MESSAGE, mimeMessageString); } /** * Test sending of MessageRequest with attachment data. */ TEST_F(HTTP2TransportTest, messageRequestWithAttachment) { // Create an attachment reader. std::vector attachment(TEST_ATTACHMENT_MESSAGE.begin(), TEST_ATTACHMENT_MESSAGE.end()); std::shared_ptr attachmentReader = avsCommon::avs::attachment::AttachmentUtils::createAttachmentReader(attachment); ASSERT_NE(attachmentReader, nullptr); setupHandlers(false, false); // Call connect(). m_http2Transport->connect(); // Deliver a 'REFRESHED' status to observers of AuthDelegateInterface. sendAuthStateRefreshed(); m_mockHttp2Connection->respondToDownchannelRequests( static_cast(HTTPResponseCode::SUCCESS_OK), false, RESPONSE_TIMEOUT); // Wait for doPostConnect(). ASSERT_TRUE(m_doPostConnected.waitFor(RESPONSE_TIMEOUT)); // Send post connect message with attachment. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); messageReq->addAttachmentReader(TEST_ATTACHMENT_FIELD, attachmentReader); m_http2Transport->sendPostConnectMessage(messageReq); // Wait for the postConnect message to become HTTP message request and HTTP body to be fully reassembled. auto postMessage = m_mockHttp2Connection->waitForPostRequest(LONG_RESPONSE_TIMEOUT); ASSERT_NE(postMessage, nullptr); // The number of MIME parts decoded should just be 2. ASSERT_EQ(postMessage->getMimeResponseSink()->getCountOfMimeParts(), 2u); // Get the first MIME part message and check it is the message sent. auto mimeMessage = postMessage->getMimeResponseSink()->getMimePart(0); std::string mimeMessageString(mimeMessage.begin(), mimeMessage.end()); ASSERT_EQ(TEST_MESSAGE, mimeMessageString); // Get the second MIME part message and check it is the attachement sent. auto mimeAttachment = postMessage->getMimeResponseSink()->getMimePart(1); std::string mimeAttachmentString(mimeAttachment.begin(), mimeAttachment.end()); ASSERT_EQ(TEST_ATTACHMENT_MESSAGE, mimeAttachmentString); } /** * Test pause of sending message when attachment buffer (SDS) empty but not closed. */ TEST_F(HTTP2TransportTest, pauseSendWhenSDSEmpty) { setupHandlers(false, false); // Call connect(). m_http2Transport->connect(); // Deliver a 'REFRESHED' status to observers of AuthDelegateInterface. sendAuthStateRefreshed(); m_mockHttp2Connection->respondToDownchannelRequests( static_cast(HTTPResponseCode::SUCCESS_OK), false, RESPONSE_TIMEOUT); // Wait for doPostConnect(). ASSERT_TRUE(m_doPostConnected.waitFor(RESPONSE_TIMEOUT)); // Send post connect message with attachment. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); AttachmentManager attMgr(AttachmentManager::AttachmentType::IN_PROCESS); std::vector attachment(TEST_ATTACHMENT_MESSAGE.begin(), TEST_ATTACHMENT_MESSAGE.end()); std::shared_ptr attachmentReader = attMgr.createReader(TEST_ATTACHMENT_ID_STRING_ONE, avsCommon::utils::sds::ReaderPolicy::NONBLOCKING); ASSERT_NE(attachmentReader, nullptr); messageReq->addAttachmentReader(TEST_ATTACHMENT_FIELD, attachmentReader); m_http2Transport->sendPostConnectMessage(messageReq); // Send the attachment in chunks in another thread. std::thread writerThread([this, &attMgr, &attachment]() { // Number of chunks the attachment will be divided into const unsigned chunks = 4; // the size of each chunk in bytes, this is the ceiling of (attachment.size / chunks) unsigned int chunkSize = (attachment.size() + chunks - 1) / chunks; auto writer = attMgr.createWriter(TEST_ATTACHMENT_ID_STRING_ONE, avsCommon::utils::sds::WriterPolicy::BLOCKING); AttachmentWriter::WriteStatus writeStatus = AttachmentWriter::WriteStatus::OK; unsigned int lastChunkSize = (attachment.size() % chunks == 0) ? chunkSize : attachment.size() % chunks; for (unsigned chunk = 0; chunk < chunks; chunk++) { writer->write( &attachment[chunk * chunkSize], (chunk == chunks - 1) ? lastChunkSize : chunkSize, &writeStatus); ASSERT_EQ(writeStatus, AttachmentWriter::WriteStatus::OK); ASSERT_TRUE(m_mockHttp2Connection->isPauseOnSendReceived(ONE_HUNDRED_MILLISECOND_DELAY)); } writer->close(); }); // Wait for the postConnect message to become HTTP message request and HTTP body to be fully reassembled. auto postMessage = m_mockHttp2Connection->waitForPostRequest(LONG_RESPONSE_TIMEOUT); ASSERT_NE(postMessage, nullptr); // The number of MIME parts decoded should just be 2. ASSERT_EQ(postMessage->getMimeResponseSink()->getCountOfMimeParts(), 2u); // Get the first MIME part message and check it is the message sent. auto mimeMessage = postMessage->getMimeResponseSink()->getMimePart(0); std::string mimeMessageString(mimeMessage.begin(), mimeMessage.end()); ASSERT_EQ(TEST_MESSAGE, mimeMessageString); // Get the second MIME part message and check it is the attachment sent. auto mimeAttachment = postMessage->getMimeResponseSink()->getMimePart(1); std::string mimeAttachmentString(mimeAttachment.begin(), mimeAttachment.end()); ASSERT_EQ(TEST_ATTACHMENT_MESSAGE, mimeAttachmentString); writerThread.join(); } /** * Test queuing MessageRequests until a response code has been received for any outstanding MessageRequest */ TEST_F(HTTP2TransportTest, messageRequestsQueuing) { authorizeAndConnect(); // Send 5 messages. std::vector> messageObservers; const unsigned messagesCount = 5; // number of test messages to Send for (unsigned messageNum = 0; messageNum < messagesCount; messageNum++) { std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); auto messageObserver = std::make_shared(); messageObservers.push_back(messageObserver); messageReq->addObserver(messageObserver); m_http2Transport->send(messageReq); } // Give m_http2Transport a chance to misbehave and send more than a single request before receiving a response. std::this_thread::sleep_for(SHORT_DELAY); // Check that only 1 out of the 5 POST messages have been in the outgoing send queue. ASSERT_EQ(m_mockHttp2Connection->getPostRequestsNum(), 1u); // Delayed 200 response for each POST request. unsigned int postsRequestsCount = 0; while (postsRequestsCount < messagesCount) { auto request = m_mockHttp2Connection->waitForPostRequest(RESPONSE_TIMEOUT); if (request) { postsRequestsCount++; // Give m_http2Transport a chance to misbehave and send requests before receiving a response. std::this_thread::sleep_for(SHORT_DELAY); request->getSink()->onReceiveResponseCode(HTTPResponseCode::SUCCESS_OK); } else { break; } } // Make sure HTTP2Transport sends out the 5 POST requests. ASSERT_EQ(postsRequestsCount, messagesCount); // On disconnect, send CANCELED response for each POST REQUEST. EXPECT_CALL(*m_mockHttp2Connection, disconnect()).WillOnce(Invoke([this]() { while (true) { auto request = m_mockHttp2Connection->dequePostRequest(); if (!request) break; request->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::CANCELLED); }; })); m_http2Transport->shutdown(); // Count the number of messages that received CANCELED or NOT_CONNECTED event. unsigned messagesCanceled = 0; for (unsigned messageNum = 0; messageNum < messagesCount; messageNum++) { if (messageObservers[messageNum]->m_status.waitFor(RESPONSE_TIMEOUT)) { switch (messageObservers[messageNum]->m_status.getValue()) { case MessageRequestObserverInterface::Status::CANCELED: case MessageRequestObserverInterface::Status::NOT_CONNECTED: messagesCanceled++; default: break; } } } ASSERT_EQ(messagesCanceled, messagesCount); } /** * Test notification of onSendCompleted (check mapping of all cases and their mapping to * MessageRequestObserverInterface::Status). */ TEST_F(HTTP2TransportTest, onSendCompletedNotification) { // Contains the mapping of HTTPResponseCode, HTTP2ResponseFinishedStatus and the expected // MessageRequestObserverInterface::Status. const std::vector> messageResponseMap = { {std::make_tuple( NO_CALL, HTTP2ResponseFinishedStatus::INTERNAL_ERROR, MessageRequestObserverInterface::Status::INTERNAL_ERROR)}, {std::make_tuple( NO_CALL, HTTP2ResponseFinishedStatus::CANCELLED, MessageRequestObserverInterface::Status::CANCELED)}, {std::make_tuple( NO_CALL, HTTP2ResponseFinishedStatus::TIMEOUT, MessageRequestObserverInterface::Status::TIMEDOUT)}, {std::make_tuple( NO_CALL, static_cast(-1), MessageRequestObserverInterface::Status::INTERNAL_ERROR)}, {std::make_tuple( HTTPResponseCode::HTTP_RESPONSE_CODE_UNDEFINED, HTTP2ResponseFinishedStatus::INTERNAL_ERROR, MessageRequestObserverInterface::Status::INTERNAL_ERROR)}, {std::make_tuple( HTTPResponseCode::SUCCESS_OK, HTTP2ResponseFinishedStatus::CANCELLED, MessageRequestObserverInterface::Status::CANCELED)}, {std::make_tuple( HTTPResponseCode::REDIRECTION_START_CODE, HTTP2ResponseFinishedStatus::TIMEOUT, MessageRequestObserverInterface::Status::TIMEDOUT)}, {std::make_tuple( HTTPResponseCode::BAD_REQUEST, static_cast(-1), MessageRequestObserverInterface::Status::INTERNAL_ERROR)}, {std::make_tuple( HTTPResponseCode::HTTP_RESPONSE_CODE_UNDEFINED, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::INTERNAL_ERROR)}, {std::make_tuple( HTTPResponseCode::SUCCESS_OK, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::SUCCESS)}, {std::make_tuple( HTTPResponseCode::SUCCESS_NO_CONTENT, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::SUCCESS_NO_CONTENT)}, {std::make_tuple( HTTPResponseCode::REDIRECTION_START_CODE, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::SERVER_OTHER_ERROR)}, {std::make_tuple( HTTPResponseCode::REDIRECTION_END_CODE, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::SERVER_OTHER_ERROR)}, {std::make_tuple( HTTPResponseCode::BAD_REQUEST, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::BAD_REQUEST)}, {std::make_tuple( HTTPResponseCode::FORBIDDEN, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::INVALID_AUTH)}, {std::make_tuple( HTTPResponseCode::SERVER_INTERNAL_ERROR, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::SERVER_INTERNAL_ERROR_V2)}, {std::make_tuple( -1, HTTP2ResponseFinishedStatus::COMPLETE, MessageRequestObserverInterface::Status::SERVER_OTHER_ERROR)}, }; authorizeAndConnect(); // Send a message for each test case defined in the messageResponseMap. std::vector> messageObservers; unsigned messagesCount = messageResponseMap.size(); // number of test messages to send for (unsigned messageNum = 0; messageNum < messagesCount; messageNum++) { std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); auto messageObserver = std::make_shared(); messageObservers.push_back(messageObserver); messageReq->addObserver(messageObserver); m_http2Transport->send(messageReq); } // Send the response code for each POST request. unsigned int postsRequestsCount = 0; for (unsigned int i = 0; i < messagesCount; i++) { auto request = m_mockHttp2Connection->waitForPostRequest(RESPONSE_TIMEOUT); if (request) { m_mockHttp2Connection->dequePostRequest(); postsRequestsCount++; auto responseCode = std::get<0>(messageResponseMap[i]); if (responseCode != NO_CALL) { request->getSink()->onReceiveResponseCode(responseCode); } auto responseFinished = std::get<1>(messageResponseMap[i]); request->getSink()->onResponseFinished(static_cast(responseFinished)); } else { break; } } // Check if we got all the POST requests. ASSERT_EQ(postsRequestsCount, messagesCount); // Check that we got the right onSendCompleted notifications. for (unsigned messageNum = 0; messageNum < messagesCount; messageNum++) { if (messageObservers[messageNum]->m_status.waitFor(RESPONSE_TIMEOUT)) { auto expectedMessageObserverStatus = std::get<2>(messageResponseMap[messageNum]); ASSERT_EQ(messageObservers[messageNum]->m_status.getValue(), expectedMessageObserverStatus); } } } /** * Test onExceptionReceived() notification for non 200 content. */ TEST_F(HTTP2TransportTest, onExceptionReceivedNon200Content) { authorizeAndConnect(); // Send a message. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); auto messageObserver = std::make_shared(); messageReq->addObserver(messageObserver); m_http2Transport->send(messageReq); auto request = m_mockHttp2Connection->waitForPostRequest(RESPONSE_TIMEOUT); ASSERT_NE(request, nullptr); request->getSink()->onReceiveResponseCode(HTTPResponseCode::SERVER_INTERNAL_ERROR); request->getSink()->onReceiveData(NON_MIME_PAYLOAD.c_str(), NON_MIME_PAYLOAD.size()); request->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::COMPLETE); ASSERT_TRUE(messageObserver->m_exception.waitFor(RESPONSE_TIMEOUT)); ASSERT_EQ(messageObserver->m_exception.getValue(), NON_MIME_PAYLOAD); ASSERT_TRUE(messageObserver->m_status.waitFor(RESPONSE_TIMEOUT)); ASSERT_EQ(messageObserver->m_status.getValue(), MessageRequestObserverInterface::Status::SERVER_INTERNAL_ERROR_V2); } /** * Test MessageConsumerInterface receipt of directives on Downchannel and Event streams. */ TEST_F(HTTP2TransportTest, messageConsumerReceiveDirective) { PromiseFuturePair messagesAreConsumed; unsigned int consumedMessageCount = 0; std::vector messages; authorizeAndConnect(); EXPECT_CALL(*m_mockMessageConsumer, consumeMessage(_, _)) .WillRepeatedly(Invoke([&messagesAreConsumed, &consumedMessageCount, &messages]( const std::string& contextId, const std::string& message) { consumedMessageCount++; messages.push_back(message); if (consumedMessageCount == 2u) { messagesAreConsumed.setValue(); } }) ); // Send a message. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); auto messageObserver = std::make_shared(); messageReq->addObserver(messageObserver); m_http2Transport->send(messageReq); auto eventStream = m_mockHttp2Connection->waitForPostRequest(RESPONSE_TIMEOUT); ASSERT_NE(eventStream, nullptr); eventStream->getSink()->onReceiveResponseCode(HTTPResponseCode::SUCCESS_OK); eventStream->getSink()->onReceiveHeaderLine(HTTP_BOUNDARY_HEADER); eventStream->getSink()->onReceiveData(MIME_BODY_DIRECTIVE1.c_str(), MIME_BODY_DIRECTIVE1.size()); eventStream->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::COMPLETE); auto downchannelStream = m_mockHttp2Connection->getDownchannelRequest(); ASSERT_NE(downchannelStream, nullptr); downchannelStream->getSink()->onReceiveResponseCode(HTTPResponseCode::SUCCESS_OK); downchannelStream->getSink()->onReceiveHeaderLine(HTTP_BOUNDARY_HEADER); downchannelStream->getSink()->onReceiveData(MIME_BODY_DIRECTIVE2.c_str(), MIME_BODY_DIRECTIVE2.size()); ASSERT_TRUE(messagesAreConsumed.waitFor(RESPONSE_TIMEOUT)); ASSERT_EQ(messages[0], DIRECTIVE1); ASSERT_EQ(messages[1], DIRECTIVE2); } /** * Test broadcast onServerSideDisconnect() upon closure of successfully opened downchannel. */ TEST_F(HTTP2TransportTest, onServerSideDisconnectOnDownchannelClosure) { authorizeAndConnect(); // Send a message. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); m_http2Transport->send(messageReq); PromiseFuturePair gotOnServerSideDisconnect; auto setGotOnServerSideDisconnect = [&gotOnServerSideDisconnect] { gotOnServerSideDisconnect.setValue(); }; PromiseFuturePair gotOnDisconnected; auto setGotOnDisconnected = [&gotOnDisconnected] { gotOnDisconnected.setValue(); }; // Expect disconnect events later when downchannel receives a COMPLETE finished response. { InSequence dummy; EXPECT_CALL(*m_mockTransportObserver, onServerSideDisconnect(_)) .Times(1) .WillOnce(InvokeWithoutArgs(setGotOnServerSideDisconnect)); EXPECT_CALL(*m_mockTransportObserver, onDisconnected(_, _)) .Times(1) .WillOnce(InvokeWithoutArgs(setGotOnDisconnected)); } // Upon receiving the message, HTTP2Connection/request will reply to the down-channel request with // onResponseFinished(COMPLETE). auto eventStream = m_mockHttp2Connection->waitForPostRequest(RESPONSE_TIMEOUT); ASSERT_NE(eventStream, nullptr); auto downchannelStream = m_mockHttp2Connection->getDownchannelRequest(); downchannelStream->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::COMPLETE); // Wait for onResponseFinished() to be handled. ASSERT_TRUE(gotOnServerSideDisconnect.waitFor(RESPONSE_TIMEOUT)); ASSERT_TRUE(gotOnDisconnected.waitFor(RESPONSE_TIMEOUT)); } /** * Test detection of MessageRequest timeout and trigger of ping request. */ TEST_F(HTTP2TransportTest, messageRequestTimeoutPingRequest) { authorizeAndConnect(); // Send a message. std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE, ""); m_http2Transport->send(messageReq); // Upon receiving the message, the mock HTTP2Connection/request will reply to the request with // onResponseFinished(TIMEOUT). auto eventStream = m_mockHttp2Connection->waitForPostRequest(RESPONSE_TIMEOUT); ASSERT_NE(eventStream, nullptr); eventStream->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::TIMEOUT); // Wait for a long time (up to 5 seconds) for the mock HTTP2Connection to receive a ping request. ASSERT_NE(m_mockHttp2Connection->waitForPingRequest(RESPONSE_TIMEOUT), nullptr); } /** * Test detection of network inactivity and trigger of ping request and continued ping for continued inactivity. */ TEST_F(HTTP2TransportTest, networkInactivityPingRequest) { // Short time to wait for inactivity before sending a ping. static const std::chrono::seconds testInactivityTimeout = SHORT_DELAY; // This test just checks that a second and third ping will be sen static const unsigned expectedInactivityPingCount{3u}; // How long until pings should be sent plus some extra time to allow notifications to be processed. static const std::chrono::seconds testInactivityTime = {testInactivityTimeout * expectedInactivityPingCount + SHORT_DELAY}; // Setup HTTP2Transport with shorter ping inactivity timeout. HTTP2Transport::Configuration cfg; cfg.inactivityTimeout = testInactivityTimeout; m_http2Transport = HTTP2Transport::create( m_mockAuthDelegate, TEST_AVS_ENDPOINT_STRING, m_mockHttp2Connection, m_mockMessageConsumer, m_attachmentManager, m_mockTransportObserver, m_mockPostConnectFactory, cfg); authorizeAndConnect(); PromiseFuturePair gotPings; // Respond 204 to ping requests. std::thread pingResponseThread([this, &gotPings]() { unsigned pingCount{0}; while (pingCount < expectedInactivityPingCount) { auto pingRequest = m_mockHttp2Connection->waitForPingRequest(RESPONSE_TIMEOUT); if (!pingRequest) { continue; } m_mockHttp2Connection->dequePingRequest(); pingRequest->getSink()->onReceiveResponseCode(HTTPResponseCode::SUCCESS_NO_CONTENT); pingRequest->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::COMPLETE); pingCount++; } gotPings.setValue(); }); ASSERT_TRUE(gotPings.waitFor(testInactivityTime)); pingResponseThread.join(); } /** * Test connection tear down for ping timeout. */ TEST_F(HTTP2TransportTest, tearDownPingTimeout) { // Short time to wait for inactivity before sending a ping. const std::chrono::seconds testInactivityTimeout = SHORT_DELAY; // Setup HTTP2Transport with shorter ping inactivity timeout. HTTP2Transport::Configuration cfg; cfg.inactivityTimeout = testInactivityTimeout; m_http2Transport = HTTP2Transport::create( m_mockAuthDelegate, TEST_AVS_ENDPOINT_STRING, m_mockHttp2Connection, m_mockMessageConsumer, m_attachmentManager, m_mockTransportObserver, m_mockPostConnectFactory, cfg); authorizeAndConnect(); PromiseFuturePair gotOnDisconnected; auto setGotOnDisconnected = [&gotOnDisconnected] { gotOnDisconnected.setValue(); }; EXPECT_CALL(*m_mockTransportObserver, onDisconnected(_, _)) .Times(1) .WillOnce(InvokeWithoutArgs(setGotOnDisconnected)); // Reply to a ping request. std::thread pingThread([this]() { auto pingRequest = m_mockHttp2Connection->waitForPingRequest(RESPONSE_TIMEOUT); ASSERT_TRUE(pingRequest); m_mockHttp2Connection->dequePingRequest(); pingRequest->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::TIMEOUT); }); ASSERT_TRUE(gotOnDisconnected.waitFor(RESPONSE_TIMEOUT)); pingThread.join(); } /** * Test connection tear down for ping failure. */ TEST_F(HTTP2TransportTest, tearDownPingFailure) { // Short time to wait for inactivity before sending a ping. const std::chrono::seconds testInactivityTimeout = SHORT_DELAY; // Setup HTTP2Transport with shorter ping inactivity timeout. HTTP2Transport::Configuration cfg; cfg.inactivityTimeout = testInactivityTimeout; m_http2Transport = HTTP2Transport::create( m_mockAuthDelegate, TEST_AVS_ENDPOINT_STRING, m_mockHttp2Connection, m_mockMessageConsumer, m_attachmentManager, m_mockTransportObserver, m_mockPostConnectFactory, cfg); authorizeAndConnect(); PromiseFuturePair gotOnDisconnected; auto setGotOnDisconnected = [&gotOnDisconnected] { gotOnDisconnected.setValue(); }; EXPECT_CALL(*m_mockTransportObserver, onDisconnected(_, _)) .Times(1) .WillOnce(InvokeWithoutArgs(setGotOnDisconnected)); // Reply to a ping request. std::thread pingThread([this]() { auto pingRequest = m_mockHttp2Connection->waitForPingRequest(RESPONSE_TIMEOUT); ASSERT_TRUE(pingRequest); m_mockHttp2Connection->dequePingRequest(); pingRequest->getSink()->onReceiveResponseCode(HTTPResponseCode::BAD_REQUEST); pingRequest->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::COMPLETE); }); ASSERT_TRUE(gotOnDisconnected.waitFor(RESPONSE_TIMEOUT)); pingThread.join(); } /** * Test limiting use of AVS streams to 10. */ TEST_F(HTTP2TransportTest, avsStreamsLimit) { // Number of test messages to send for this test. This number is much larger than MAX_POST_STREAMS // to assure that this test exercises the case where more requests are outstanding than are allowed // to be sent at one time, forcing HTTP2Transport to queue the requests until some requests complete. const unsigned messagesCount = MAX_POST_STREAMS * 2; authorizeAndConnect(); m_mockHttp2Connection->setResponseToPOSTRequests(HTTPResponseCode::SUCCESS_OK); // Send the 20 messages. std::vector> messageObservers; for (unsigned messageNum = 0; messageNum < messagesCount; messageNum++) { std::shared_ptr messageReq = std::make_shared(TEST_MESSAGE + std::to_string(messageNum), ""); auto messageObserver = std::make_shared(); messageObservers.push_back(messageObserver); messageReq->addObserver(messageObserver); m_http2Transport->send(messageReq); } // Check that there was a downchannel request sent out. ASSERT_TRUE(m_mockHttp2Connection->getDownchannelRequest(RESPONSE_TIMEOUT)); // Check the messages we sent were limited. ASSERT_EQ(m_mockHttp2Connection->getPostRequestsNum(), MAX_POST_STREAMS); unsigned int completed = 0; std::shared_ptr request; while ((request = m_mockHttp2Connection->dequePostRequest(RESPONSE_TIMEOUT)) != nullptr && completed < messagesCount) { request->getSink()->onResponseFinished(HTTP2ResponseFinishedStatus::COMPLETE); completed++; // Give m_http2Transport a little time to misbehave. std::this_thread::sleep_for(TEN_MILLISECOND_DELAY); } // Check all the POST requests have been enqueued ASSERT_EQ(completed, messagesCount); // Check that the maximum number of enqueued messages at any time has been limited. ASSERT_EQ(m_mockHttp2Connection->getMaxPostRequestsEnqueud(), MAX_POST_STREAMS); } } // namespace test } // namespace transport } // namespace acl } // namespace alexaClientSDK