// Copyright 2015 The Weave 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 "src/notification/xmpp_stream_parser.h" #include <gtest/gtest.h> #include <memory> #include <vector> #include "src/notification/xml_node.h" namespace weave { namespace { // Use some real-world XMPP stream snippet to make sure all the expected // elements are parsed properly. const char kXmppStreamData[] = "<stream:stream from=\"clouddevices.gserviceaccount.com\" id=\"76EEB8FDB449" "5558\" version=\"1.0\" xmlns:stream=\"http://etherx.jabber.org/streams\" x" "mlns=\"jabber:client\">" "<stream:features><starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"><requ" "ired/></starttls><mechanisms xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\"><m" "echanism>X-OAUTH2</mechanism><mechanism>X-GOOGLE-TOKEN</mechanism></mechan" "isms></stream:features>" "<message from=\"cloud-devices@clouddevices.google.com/srvenc-xgbCfg9hX6tCp" "xoMYsExqg==\" to=\"4783f652b387449fc52a76f9a16e616f@clouddevices.gservicea" "ccount.com/5A85ED9C\"><push:push channel=\"cloud_devices\" xmlns:push=\"go" "ogle:push\"><push:recipient to=\"4783f652b387449fc52a76f9a16e616f@clouddev" "ices.gserviceaccount.com\"></push:recipient><push:data>eyJraW5kIjoiY2xvdWR" "kZXZpY2VzI25vdGlmaWNhdGlvbiIsInR5cGUiOiJDT01NQU5EX0NSRUFURUQiLCJjb21tYW5kS" "WQiOiIwNWE3MTA5MC1hZWE4LWMzNzQtOTYwNS0xZTRhY2JhNDRmM2Y4OTAzZmM3Yy01NjExLWI" "5ODAtOTkyMy0yNjc2YjYwYzkxMGMiLCJkZXZpY2VJZCI6IjA1YTcxMDkwLWFlYTgtYzM3NC05N" "jA1LTFlNGFjYmE0NGYzZiIsImNvbW1hbmQiOnsia2luZCI6ImNsb3VkZGV2aWNlcyNjb21tYW5" "kIiwiaWQiOiIwNWE3MTA5MC1hZWE4LWMzNzQtOTYwNS0xZTRhY2JhNDRmM2Y4OTAzZmM3Yy01N" "jExLWI5ODAtOTkyMy0yNjc2YjYwYzkxMGMiLCJkZXZpY2VJZCI6IjA1YTcxMDkwLWFlYTgtYzM" "3NC05NjA1LTFlNGFjYmE0NGYzZiIsIm5hbWUiOiJiYXNlLl9qdW1wIiwic3RhdGUiOiJxdWV1Z" "WQiLCJlcnJvciI6eyJhcmd1bWVudHMiOltdfSwiY3JlYXRpb25UaW1lTXMiOiIxNDMxNTY0NDY" "4MjI3IiwiZXhwaXJhdGlvblRpbWVNcyI6IjE0MzE1NjgwNjgyMjciLCJleHBpcmF0aW9uVGltZ" "W91dE1zIjoiMzYwMDAwMCJ9fQ==</push:data></push:push></message>"; } // anonymous namespace class XmppStreamParserTest : public testing::Test, public XmppStreamParser::Delegate { public: void SetUp() override { parser_.reset(new XmppStreamParser{this}); } void OnStreamStart(const std::string& node_name, std::map<std::string, std::string> attributes) override { EXPECT_FALSE(stream_started_); stream_started_ = true; stream_start_node_name_ = node_name; stream_start_node_attributes_ = std::move(attributes); } void OnStreamEnd(const std::string& node_name) override { EXPECT_TRUE(stream_started_); EXPECT_EQ(stream_start_node_name_, node_name); stream_started_ = false; } void OnStanza(std::unique_ptr<XmlNode> stanza) override { stanzas_.push_back(std::move(stanza)); } void Reset() { parser_.reset(new XmppStreamParser{this}); stream_started_ = false; stream_start_node_name_.clear(); stream_start_node_attributes_.clear(); stanzas_.clear(); } std::unique_ptr<XmppStreamParser> parser_; bool stream_started_{false}; std::string stream_start_node_name_; std::map<std::string, std::string> stream_start_node_attributes_; std::vector<std::unique_ptr<XmlNode>> stanzas_; }; TEST_F(XmppStreamParserTest, InitialState) { EXPECT_FALSE(stream_started_); EXPECT_TRUE(stream_start_node_name_.empty()); EXPECT_TRUE(stream_start_node_attributes_.empty()); EXPECT_TRUE(stanzas_.empty()); } TEST_F(XmppStreamParserTest, FullStartElement) { parser_->ParseData("<foo bar=\"baz\" quux=\"1\">"); EXPECT_TRUE(stream_started_); EXPECT_EQ("foo", stream_start_node_name_); const std::map<std::string, std::string> expected_attrs{{"bar", "baz"}, {"quux", "1"}}; EXPECT_EQ(expected_attrs, stream_start_node_attributes_); } TEST_F(XmppStreamParserTest, PartialStartElement) { parser_->ParseData("<foo bar=\"baz"); EXPECT_FALSE(stream_started_); EXPECT_TRUE(stream_start_node_name_.empty()); EXPECT_TRUE(stream_start_node_attributes_.empty()); EXPECT_TRUE(stanzas_.empty()); parser_->ParseData("\" quux"); EXPECT_FALSE(stream_started_); parser_->ParseData("=\"1\">"); EXPECT_TRUE(stream_started_); EXPECT_EQ("foo", stream_start_node_name_); const std::map<std::string, std::string> expected_attrs{{"bar", "baz"}, {"quux", "1"}}; EXPECT_EQ(expected_attrs, stream_start_node_attributes_); } TEST_F(XmppStreamParserTest, VariableLengthPackets) { std::string value; const std::string xml_data = kXmppStreamData; const std::map<std::string, std::string> expected_stream_attrs{ {"from", "clouddevices.gserviceaccount.com"}, {"id", "76EEB8FDB4495558"}, {"version", "1.0"}, {"xmlns:stream", "http://etherx.jabber.org/streams"}, {"xmlns", "jabber:client"}}; // Try splitting the data into pieces from 1 character in size to the whole // data block and verify that we still can parse the whole message correctly. // Here |step| is the size of each individual data chunk. for (size_t step = 1; step <= xml_data.size(); step++) { // Feed each individual chunk to the parser and hope it can piece everything // together correctly. for (size_t pos = 0; pos < xml_data.size(); pos += step) { parser_->ParseData(xml_data.substr(pos, step)); } EXPECT_TRUE(stream_started_); EXPECT_EQ("stream:stream", stream_start_node_name_); EXPECT_EQ(expected_stream_attrs, stream_start_node_attributes_); EXPECT_EQ(2u, stanzas_.size()); const XmlNode* stanza1 = stanzas_[0].get(); EXPECT_EQ("stream:features", stanza1->name()); ASSERT_EQ(2u, stanza1->children().size()); const XmlNode* child1 = stanza1->children()[0].get(); EXPECT_EQ("starttls", child1->name()); ASSERT_EQ(1u, child1->children().size()); EXPECT_EQ("required", child1->children()[0]->name()); const XmlNode* child2 = stanza1->children()[1].get(); EXPECT_EQ("mechanisms", child2->name()); ASSERT_EQ(2u, child2->children().size()); EXPECT_EQ("mechanism", child2->children()[0]->name()); EXPECT_EQ("X-OAUTH2", child2->children()[0]->text()); EXPECT_EQ("mechanism", child2->children()[1]->name()); EXPECT_EQ("X-GOOGLE-TOKEN", child2->children()[1]->text()); const XmlNode* stanza2 = stanzas_[1].get(); EXPECT_EQ("message", stanza2->name()); ASSERT_EQ(2u, stanza2->attributes().size()); EXPECT_TRUE(stanza2->GetAttribute("from", &value)); EXPECT_EQ( "cloud-devices@clouddevices.google.com/" "srvenc-xgbCfg9hX6tCpxoMYsExqg==", value); EXPECT_TRUE(stanza2->GetAttribute("to", &value)); EXPECT_EQ( "4783f652b387449fc52a76f9a16e616f@clouddevices.gserviceaccount." "com/5A85ED9C", value); ASSERT_EQ(1u, stanza2->children().size()); const XmlNode* child = stanza2->children().back().get(); EXPECT_EQ("push:push", child->name()); ASSERT_EQ(2u, child->attributes().size()); EXPECT_TRUE(child->GetAttribute("channel", &value)); EXPECT_EQ("cloud_devices", value); EXPECT_TRUE(child->GetAttribute("xmlns:push", &value)); EXPECT_EQ("google:push", value); ASSERT_EQ(2u, child->children().size()); child1 = child->children()[0].get(); EXPECT_EQ("push:recipient", child1->name()); ASSERT_EQ(1u, child1->attributes().size()); EXPECT_TRUE(child1->GetAttribute("to", &value)); EXPECT_EQ( "4783f652b387449fc52a76f9a16e616f@clouddevices.gserviceaccount." "com", value); EXPECT_TRUE(child1->children().empty()); child2 = child->children()[1].get(); EXPECT_EQ("push:data", child2->name()); EXPECT_TRUE(child2->attributes().empty()); EXPECT_TRUE(child2->children().empty()); const std::string expected_data = "eyJraW5kIjoiY2xvdWRkZXZpY2VzI25vdGlmaWNh" "dGlvbiIsInR5cGUiOiJDT01NQU5EX0NSRUFURUQiLCJjb21tYW5kSWQiOiIwNWE3MTA5MC" "1hZWE4LWMzNzQtOTYwNS0xZTRhY2JhNDRmM2Y4OTAzZmM3Yy01NjExLWI5ODAtOTkyMy0y" "Njc2YjYwYzkxMGMiLCJkZXZpY2VJZCI6IjA1YTcxMDkwLWFlYTgtYzM3NC05NjA1LTFlNG" "FjYmE0NGYzZiIsImNvbW1hbmQiOnsia2luZCI6ImNsb3VkZGV2aWNlcyNjb21tYW5kIiwi" "aWQiOiIwNWE3MTA5MC1hZWE4LWMzNzQtOTYwNS0xZTRhY2JhNDRmM2Y4OTAzZmM3Yy01Nj" "ExLWI5ODAtOTkyMy0yNjc2YjYwYzkxMGMiLCJkZXZpY2VJZCI6IjA1YTcxMDkwLWFlYTgt" "YzM3NC05NjA1LTFlNGFjYmE0NGYzZiIsIm5hbWUiOiJiYXNlLl9qdW1wIiwic3RhdGUiOi" "JxdWV1ZWQiLCJlcnJvciI6eyJhcmd1bWVudHMiOltdfSwiY3JlYXRpb25UaW1lTXMiOiIx" "NDMxNTY0NDY4MjI3IiwiZXhwaXJhdGlvblRpbWVNcyI6IjE0MzE1NjgwNjgyMjciLCJleH" "BpcmF0aW9uVGltZW91dE1zIjoiMzYwMDAwMCJ9fQ=="; EXPECT_EQ(expected_data, child2->text()); } } } // namespace weave