// 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