blob: 6f045707c7d9f421e09501e464dc1c2361f9ccd2 [file] [log] [blame]
// Copyright 2019 The Chromium 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 "chrome/browser/sharing/web_push/web_push_sender.h"
#include "base/base64.h"
#include "base/base64url.h"
#include "base/callback_helpers.h"
#include "base/json/json_reader.h"
#include "content/public/test/browser_task_environment.h"
#include "crypto/ec_private_key.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/re2/src/re2/re2.h"
namespace {
// An ASN.1-encoded PrivateKeyInfo block from PKCS #8.
const char kPrivateKey[] =
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgS8wRbDOWz0lKExvIVQiRKtPAP8"
"dgHUHAw5gyOd5d4jKhRANCAARZb49Va5MD/KcWtc0oiWc2e8njBDtQzj0mzcOl1fDSt16Pvu6p"
"fTU3MTWnImDNnkPxtXm58K7Uax8jFxA4TeXJ";
} // namespace
WebPushMessage CreateMessage() {
WebPushMessage message;
message.time_to_live = 3600;
message.payload = "payload";
return message;
}
class WebPushSenderTest : public testing::Test {
public:
WebPushSenderTest();
~WebPushSenderTest() override;
void SetUp() override;
WebPushSender* sender() { return sender_.get(); }
network::TestURLLoaderFactory& loader() { return test_url_loader_factory_; }
void OnMessageSent(SendWebPushMessageResult* result_out,
base::Optional<std::string>* message_id_out,
SendWebPushMessageResult result,
base::Optional<std::string> message_id) {
*result_out = result;
*message_id_out = message_id;
}
private:
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
network::TestURLLoaderFactory test_url_loader_factory_;
std::unique_ptr<WebPushSender> sender_;
};
WebPushSenderTest::WebPushSenderTest() = default;
WebPushSenderTest::~WebPushSenderTest() = default;
void WebPushSenderTest::SetUp() {
sender_ = std::make_unique<WebPushSender>(
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
&test_url_loader_factory_));
}
TEST_F(WebPushSenderTest, SendMessageTest) {
std::string private_key_info;
ASSERT_TRUE(base::Base64Decode(kPrivateKey, &private_key_info));
std::unique_ptr<crypto::ECPrivateKey> private_key =
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(std::vector<uint8_t>(
private_key_info.begin(), private_key_info.end()));
ASSERT_TRUE(private_key);
SendWebPushMessageResult result;
base::Optional<std::string> message_id;
sender()->SendMessage(
"fcm_token", private_key.get(), CreateMessage(),
base::BindOnce(&WebPushSenderTest::OnMessageSent, base::Unretained(this),
&result, &message_id));
ASSERT_EQ(loader().NumPending(), 1);
network::TestURLLoaderFactory::PendingRequest* pendingRequest =
loader().GetPendingRequest(0);
EXPECT_EQ("https://fcm.googleapis.com/fcm/send/fcm_token",
pendingRequest->request.url);
EXPECT_EQ("POST", pendingRequest->request.method);
net::HttpRequestHeaders headers = pendingRequest->request.headers;
std::string auth_header, time_to_live, encoding;
ASSERT_TRUE(
headers.GetHeader(net::HttpRequestHeaders::kAuthorization, &auth_header));
const std::string expected_header = "vapid t=(.*), k=(.*)";
std::string jwt, base64_public_key;
ASSERT_TRUE(re2::RE2::FullMatch(auth_header, expected_header, &jwt,
&base64_public_key));
// Make sure JWT dat can be decomposed
std::string::size_type dot_position = jwt.rfind(".");
ASSERT_NE(std::string::npos, dot_position);
std::string data = jwt.substr(0, dot_position);
std::string::size_type data_dot_position = data.find(".");
ASSERT_NE(std::string::npos, data_dot_position);
std::string payload_decoded;
ASSERT_TRUE(base::Base64UrlDecode(data.substr(data_dot_position + 1),
base::Base64UrlDecodePolicy::IGNORE_PADDING,
&payload_decoded));
base::Optional<base::Value> payload_value =
base::JSONReader::Read(payload_decoded);
ASSERT_TRUE(payload_value);
ASSERT_TRUE(payload_value->is_dict());
EXPECT_EQ(base::Value("https://fcm.googleapis.com"),
payload_value->ExtractKey("aud"));
int secondsSinceEpoch =
(base::Time::Now() - base::Time::UnixEpoch()).InSeconds();
EXPECT_EQ(base::Value(secondsSinceEpoch + 12 * 60 * 60),
payload_value->ExtractKey("exp"));
// Make sure public key can be base64 url decoded
std::string public_key;
EXPECT_TRUE(base::Base64UrlDecode(base64_public_key,
base::Base64UrlDecodePolicy::IGNORE_PADDING,
&public_key));
ASSERT_TRUE(headers.GetHeader("TTL", &time_to_live));
EXPECT_EQ("3600", time_to_live);
ASSERT_TRUE(headers.GetHeader("content-encoding", &encoding));
EXPECT_EQ("aes128gcm", encoding);
const std::vector<network::DataElement>* body_elements =
pendingRequest->request.request_body->elements();
ASSERT_EQ(1UL, body_elements->size());
const network::DataElement& body = body_elements->back();
ASSERT_EQ(network::DataElement::Tag::kBytes, body.type());
EXPECT_EQ("payload", body.As<network::DataElementBytes>().AsStringPiece());
auto response_head = network::CreateURLResponseHead(net::HTTP_OK);
response_head->headers->AddHeader("location",
"https://fcm.googleapis.com/message_id");
loader().SimulateResponseForPendingRequest(
pendingRequest->request.url, network::URLLoaderCompletionStatus(net::OK),
std::move(response_head), "");
EXPECT_EQ(SendWebPushMessageResult::kSuccessful, result);
EXPECT_EQ("message_id", message_id);
}
struct WebPushUrgencyTestData {
const WebPushMessage::Urgency urgency;
const std::string expected_header;
} kWebPushUrgencyTestData[] = {
{WebPushMessage::Urgency::kVeryLow, "very-low"},
{WebPushMessage::Urgency::kLow, "low"},
{WebPushMessage::Urgency::kNormal, "normal"},
{WebPushMessage::Urgency::kHigh, "high"},
};
class WebPushUrgencyTest
: public WebPushSenderTest,
public testing::WithParamInterface<WebPushUrgencyTestData> {};
TEST_P(WebPushUrgencyTest, SetUrgencyTest) {
std::string private_key_info;
ASSERT_TRUE(base::Base64Decode(kPrivateKey, &private_key_info));
std::unique_ptr<crypto::ECPrivateKey> private_key =
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(std::vector<uint8_t>(
private_key_info.begin(), private_key_info.end()));
base::Optional<std::string> message_id;
std::string urgency;
WebPushMessage message = CreateMessage();
message.urgency = GetParam().urgency;
sender()->SendMessage("token", private_key.get(), std::move(message),
base::DoNothing());
ASSERT_EQ(loader().NumPending(), 1);
net::HttpRequestHeaders headers =
loader().GetPendingRequest(0)->request.headers;
ASSERT_TRUE(headers.GetHeader("Urgency", &urgency));
ASSERT_EQ(GetParam().expected_header, urgency);
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
WebPushUrgencyTest,
testing::ValuesIn(kWebPushUrgencyTestData));
struct WebPushHttpStatusTestData {
const net::Error error_code;
const net::HttpStatusCode http_status;
const SendWebPushMessageResult expected_result;
} kWebPushHttpStatusTestData[] = {
{net::ERR_INSUFFICIENT_RESOURCES, net::HTTP_OK,
SendWebPushMessageResult::kVapidKeyInvalid},
{net::ERR_ABORTED, net::HTTP_OK, SendWebPushMessageResult::kNetworkError},
{net::OK, net::HTTP_OK,
SendWebPushMessageResult::kParseResponseFailed}, // As no header is set
{net::OK, net::HTTP_INTERNAL_SERVER_ERROR,
SendWebPushMessageResult::kServerError},
{net::OK, net::HTTP_NOT_FOUND, SendWebPushMessageResult::kDeviceGone},
{net::OK, net::HTTP_GONE, SendWebPushMessageResult::kDeviceGone},
{net::OK, net::HTTP_BAD_REQUEST,
SendWebPushMessageResult::kPayloadTooLarge},
{net::OK, net::HTTP_REQUEST_ENTITY_TOO_LARGE,
SendWebPushMessageResult::kPayloadTooLarge},
};
class WebPushHttpStatusTest
: public WebPushSenderTest,
public testing::WithParamInterface<WebPushHttpStatusTestData> {};
TEST_P(WebPushHttpStatusTest, HttpStatusTest) {
std::string private_key_info;
ASSERT_TRUE(base::Base64Decode(kPrivateKey, &private_key_info));
std::unique_ptr<crypto::ECPrivateKey> private_key =
crypto::ECPrivateKey::CreateFromPrivateKeyInfo(std::vector<uint8_t>(
private_key_info.begin(), private_key_info.end()));
ASSERT_TRUE(private_key);
SendWebPushMessageResult result;
base::Optional<std::string> message_id;
sender()->SendMessage(
"fcm_token", private_key.get(), CreateMessage(),
base::BindOnce(&WebPushSenderTest::OnMessageSent, base::Unretained(this),
&result, &message_id));
ASSERT_EQ(loader().NumPending(), 1);
loader().SimulateResponseForPendingRequest(
loader().GetPendingRequest(0)->request.url,
network::URLLoaderCompletionStatus(GetParam().error_code),
network::CreateURLResponseHead(GetParam().http_status), "");
ASSERT_EQ(GetParam().expected_result, result);
ASSERT_FALSE(message_id);
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
WebPushHttpStatusTest,
testing::ValuesIn(kWebPushHttpStatusTestData));