buffet: Add correct handling of XMPP IQ stanzas
Implemented a more intelligent handling of IQ requests and responses.
Each time an IQ request is sent, a new unique request ID is generated
and then a response with the same ID is expected. If no reponse is
received within a timeout interval (of 30 seconds) a timeout callback
is called allowing the caller to handle this event correctly.
Changed the XMPP connection handshake implementation which used some
of IQ stanza exchange with the server to use the new IqStanzaHandler
class.
BUG=brillo:1138
TEST=`FEATURES=test emerge-link buffet`
Change-Id: I9534169466159d7531e5f01a25a0583ca6b341c3
Reviewed-on: https://chromium-review.googlesource.com/274446
Trybot-Ready: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Vitaly Buka <vitalybuka@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/notification/xmpp_iq_stanza_handler_unittest.cc b/buffet/notification/xmpp_iq_stanza_handler_unittest.cc
new file mode 100644
index 0000000..1e8be42
--- /dev/null
+++ b/buffet/notification/xmpp_iq_stanza_handler_unittest.cc
@@ -0,0 +1,222 @@
+// Copyright 2015 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/notification/xmpp_iq_stanza_handler.h"
+
+#include <map>
+#include <memory>
+
+#include <chromeos/bind_lambda.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "buffet/notification/xml_node.h"
+#include "buffet/notification/xmpp_channel.h"
+#include "buffet/notification/xmpp_stream_parser.h"
+
+using testing::Invoke;
+using testing::Return;
+using testing::_;
+
+namespace buffet {
+namespace {
+
+// Mock-like task runner that allow the tests to inspect the calls to
+// TaskRunner::PostDelayedTask and verify the delays.
+class TestTaskRunner : public base::SingleThreadTaskRunner {
+ public:
+ TestTaskRunner() = default;
+
+ MOCK_METHOD3(PostDelayedTask, bool(const tracked_objects::Location&,
+ const base::Closure&,
+ base::TimeDelta));
+ MOCK_METHOD3(PostNonNestableDelayedTask,
+ bool(const tracked_objects::Location&,
+ const base::Closure&,
+ base::TimeDelta));
+
+ bool RunsTasksOnCurrentThread() const { return true; }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(TestTaskRunner);
+};
+
+class MockXmppChannelInterface : public XmppChannelInterface {
+ public:
+ MockXmppChannelInterface() = default;
+
+ MOCK_METHOD1(SendMessage, void(const std::string&));
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MockXmppChannelInterface);
+};
+
+// Simple class that allows to parse XML from string to XmlNode.
+class XmlParser : public XmppStreamParser::Delegate {
+ public:
+ std::unique_ptr<XmlNode> Parse(const std::string& xml) {
+ parser_.ParseData(xml);
+ return std::move(node_);
+ }
+
+ private:
+ // Overrides from XmppStreamParser::Delegate.
+ void OnStreamStart(const std::string& node_name,
+ std::map<std::string, std::string> attributes) override {
+ node_.reset(new XmlNode{node_name, std::move(attributes)});
+ }
+
+ void OnStreamEnd(const std::string& node_name) override {}
+
+ void OnStanza(std::unique_ptr<XmlNode> stanza) override {
+ node_->AddChild(std::move(stanza));
+ }
+
+ std::unique_ptr<XmlNode> node_;
+ XmppStreamParser parser_{this};
+};
+
+class MockResponseReceiver {
+ public:
+ MOCK_METHOD2(OnResponse, void(int id, const std::string&));
+
+ IqStanzaHandler::ResponseCallback callback(int id) {
+ return base::Bind(&MockResponseReceiver::OnResponseCallback,
+ base::Unretained(this), id);
+ }
+
+ private:
+ void OnResponseCallback(int id, std::unique_ptr<XmlNode> response) {
+ OnResponse(id, response->children().front()->name());
+ }
+};
+
+// Functor to call the |task| immediately.
+struct TaskInvoker {
+ bool operator()(const tracked_objects::Location& location,
+ const base::Closure& task,
+ base::TimeDelta delay) {
+ task.Run();
+ return true;
+ }
+};
+
+} // anonymous namespace
+
+class IqStanzaHandlerTest : public testing::Test {
+ public:
+ void SetUp() override {
+ task_runner_ = new TestTaskRunner;
+ iq_stanza_handler_.reset(
+ new IqStanzaHandler{&mock_xmpp_channel_, task_runner_});
+ }
+
+ testing::StrictMock<MockXmppChannelInterface> mock_xmpp_channel_;
+ scoped_refptr<TestTaskRunner> task_runner_;
+ std::unique_ptr<IqStanzaHandler> iq_stanza_handler_;
+ MockResponseReceiver receiver_;
+ TaskInvoker task_invoker_;
+};
+
+TEST_F(IqStanzaHandlerTest, SendRequest) {
+ EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, _))
+ .WillRepeatedly(Return(true));
+
+ std::string expected_msg = "<iq id='1' type='set'><body/></iq>";
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(expected_msg)).Times(1);
+ iq_stanza_handler_->SendRequest("set", "", "", "<body/>", {}, {});
+
+ expected_msg = "<iq id='2' type='get'><body/></iq>";
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(expected_msg)).Times(1);
+ iq_stanza_handler_->SendRequest("get", "", "", "<body/>", {}, {});
+
+ expected_msg = "<iq id='3' type='query' from='foo@bar'><body/></iq>";
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(expected_msg)).Times(1);
+ iq_stanza_handler_->SendRequest("query", "foo@bar", "", "<body/>", {}, {});
+
+ expected_msg = "<iq id='4' type='query' to='foo@bar'><body/></iq>";
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(expected_msg)).Times(1);
+ iq_stanza_handler_->SendRequest("query", "", "foo@bar", "<body/>", {}, {});
+
+ expected_msg = "<iq id='5' type='query' from='foo@bar' to='baz'><body/></iq>";
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(expected_msg)).Times(1);
+ iq_stanza_handler_->SendRequest("query", "foo@bar", "baz", "<body/>", {}, {});
+}
+
+TEST_F(IqStanzaHandlerTest, UnsupportedIqRequest) {
+ // Server IQ requests are not supported for now. Expect an error response.
+ std::string expected_msg =
+ "<iq id='1' type='error'><error type='modify'>"
+ "<feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
+ "</error></iq>";
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(expected_msg)).Times(1);
+ auto request = XmlParser{}.Parse("<iq id='1' type='set'><foo/></iq>");
+ EXPECT_TRUE(iq_stanza_handler_->HandleIqStanza(std::move(request)));
+}
+
+TEST_F(IqStanzaHandlerTest, UnknownResponseId) {
+ // No requests with ID=100 have been previously sent.
+ auto request = XmlParser{}.Parse("<iq id='100' type='result'><foo/></iq>");
+ EXPECT_TRUE(iq_stanza_handler_->HandleIqStanza(std::move(request)));
+}
+
+TEST_F(IqStanzaHandlerTest, SequentialResponses) {
+ EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, _))
+ .Times(2).WillRepeatedly(Return(true));
+
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(_)).Times(2);
+ iq_stanza_handler_->SendRequest("set", "", "", "<body/>",
+ receiver_.callback(1), {});
+ iq_stanza_handler_->SendRequest("get", "", "", "<body/>",
+ receiver_.callback(2), {});
+
+ EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, _))
+ .Times(2).WillRepeatedly(Invoke(task_invoker_));
+
+ EXPECT_CALL(receiver_, OnResponse(1, "foo"));
+ auto request = XmlParser{}.Parse("<iq id='1' type='result'><foo/></iq>");
+ EXPECT_TRUE(iq_stanza_handler_->HandleIqStanza(std::move(request)));
+
+ EXPECT_CALL(receiver_, OnResponse(2, "bar"));
+ request = XmlParser{}.Parse("<iq id='2' type='result'><bar/></iq>");
+ EXPECT_TRUE(iq_stanza_handler_->HandleIqStanza(std::move(request)));
+}
+
+TEST_F(IqStanzaHandlerTest, OutOfOrderResponses) {
+ EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, _))
+ .Times(2).WillRepeatedly(Return(true));
+
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(_)).Times(2);
+ iq_stanza_handler_->SendRequest("set", "", "", "<body/>",
+ receiver_.callback(1), {});
+ iq_stanza_handler_->SendRequest("get", "", "", "<body/>",
+ receiver_.callback(2), {});
+
+ EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, _))
+ .Times(2).WillRepeatedly(Invoke(task_invoker_));
+
+ EXPECT_CALL(receiver_, OnResponse(2, "bar"));
+ auto request = XmlParser{}.Parse("<iq id='2' type='result'><bar/></iq>");
+ EXPECT_TRUE(iq_stanza_handler_->HandleIqStanza(std::move(request)));
+
+ EXPECT_CALL(receiver_, OnResponse(1, "foo"));
+ request = XmlParser{}.Parse("<iq id='1' type='result'><foo/></iq>");
+ EXPECT_TRUE(iq_stanza_handler_->HandleIqStanza(std::move(request)));
+}
+
+TEST_F(IqStanzaHandlerTest, RequestTimeout) {
+ EXPECT_CALL(*task_runner_, PostDelayedTask(_, _, _))
+ .WillOnce(Invoke(task_invoker_));
+
+ bool called = false;
+ auto on_timeout = [&called]() { called = true; };
+
+ EXPECT_CALL(mock_xmpp_channel_, SendMessage(_)).Times(1);
+ EXPECT_FALSE(called);
+ iq_stanza_handler_->SendRequest("set", "", "", "<body/>", {},
+ base::Bind(on_timeout));
+ EXPECT_TRUE(called);
+}
+
+} // namespace buffet