657 lines
24 KiB
C++
657 lines
24 KiB
C++
// Copyright 2020 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 "cast/receiver/application_agent.h"
|
|
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "cast/common/channel/message_util.h"
|
|
#include "cast/common/channel/testing/fake_cast_socket.h"
|
|
#include "cast/common/public/message_port.h"
|
|
#include "cast/receiver/channel/static_credentials.h"
|
|
#include "cast/receiver/channel/testing/device_auth_test_helpers.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include "json/writer.h" // Included to teach gtest how to pretty-print.
|
|
#include "platform/api/time.h"
|
|
#include "platform/test/fake_task_runner.h"
|
|
#include "platform/test/paths.h"
|
|
#include "testing/util/read_file.h"
|
|
#include "util/json/json_serialization.h"
|
|
|
|
namespace openscreen {
|
|
namespace cast {
|
|
namespace {
|
|
|
|
using ::cast::channel::CastMessage;
|
|
using ::testing::_;
|
|
using ::testing::Invoke;
|
|
using ::testing::Mock;
|
|
using ::testing::Ne;
|
|
using ::testing::NiceMock;
|
|
using ::testing::NotNull;
|
|
using ::testing::Sequence;
|
|
using ::testing::StrEq;
|
|
using ::testing::StrictMock;
|
|
|
|
// Returns the location of certificate and auth challenge data files for cast
|
|
// receiver tests.
|
|
std::string GetTestDataSubdir() {
|
|
return GetTestDataPath() + "cast/receiver/channel";
|
|
}
|
|
|
|
class TestCredentialsProvider final
|
|
: public DeviceAuthNamespaceHandler::CredentialsProvider {
|
|
public:
|
|
TestCredentialsProvider() {
|
|
const std::string dir = GetTestDataSubdir();
|
|
bssl::UniquePtr<X509> parsed_cert;
|
|
TrustStore fake_trust_store;
|
|
InitStaticCredentialsFromFiles(
|
|
&creds_, &parsed_cert, &fake_trust_store, dir + "/device_key.pem",
|
|
dir + "/device_chain.pem", dir + "/device_tls.pem");
|
|
}
|
|
|
|
absl::Span<const uint8_t> GetCurrentTlsCertAsDer() final {
|
|
return absl::Span<uint8_t>(creds_.tls_cert_der);
|
|
}
|
|
const DeviceCredentials& GetCurrentDeviceCredentials() final {
|
|
return creds_.device_creds;
|
|
}
|
|
|
|
private:
|
|
StaticCredentialsProvider creds_;
|
|
};
|
|
|
|
CastMessage TestAuthChallengeMessage() {
|
|
CastMessage message;
|
|
const auto result = message.ParseFromString(
|
|
ReadEntireFileToString(GetTestDataSubdir() + "/auth_challenge.pb"));
|
|
OSP_CHECK(result);
|
|
return message;
|
|
}
|
|
|
|
class FakeApplication : public ApplicationAgent::Application,
|
|
public MessagePort::Client {
|
|
public:
|
|
FakeApplication(const char* app_id, const char* display_name)
|
|
: app_ids_({app_id}), display_name_(display_name) {
|
|
OSP_CHECK(app_ids_.front().size() == 8);
|
|
}
|
|
|
|
// These are called at the end of the Launch() and Stop() methods for
|
|
// confirming those methods were called.
|
|
MOCK_METHOD(void, DidLaunch, (Json::Value params, MessagePort* port), ());
|
|
MOCK_METHOD(void, DidStop, (), ());
|
|
|
|
// MessagePort::Client overrides.
|
|
MOCK_METHOD(void,
|
|
OnMessage,
|
|
(const std::string& source_sender_id,
|
|
const std::string& message_namespace,
|
|
const std::string& message),
|
|
(override));
|
|
MOCK_METHOD(void, OnError, (Error error), (override));
|
|
|
|
const std::vector<std::string>& GetAppIds() const override {
|
|
return app_ids_;
|
|
}
|
|
|
|
bool Launch(const std::string& app_id,
|
|
const Json::Value& app_params,
|
|
MessagePort* message_port) override {
|
|
EXPECT_EQ(GetAppIds().front(), app_id);
|
|
EXPECT_TRUE(message_port);
|
|
EXPECT_FALSE(is_launched_);
|
|
++session_id_;
|
|
is_launched_ = true;
|
|
DidLaunch(app_params, message_port);
|
|
return true;
|
|
}
|
|
|
|
std::string GetSessionId() override {
|
|
std::ostringstream oss;
|
|
if (is_launched_) {
|
|
oss << GetAppIds().front() << "-9ABC-DEF0-1234-";
|
|
oss << std::setfill('0') << std::hex << std::setw(12) << session_id_;
|
|
}
|
|
return oss.str();
|
|
}
|
|
|
|
std::string GetDisplayName() override { return display_name_; }
|
|
|
|
std::vector<std::string> GetSupportedNamespaces() override {
|
|
return namespaces_;
|
|
}
|
|
void SetSupportedNamespaces(std::vector<std::string> the_namespaces) {
|
|
namespaces_ = std::move(the_namespaces);
|
|
}
|
|
|
|
void Stop() override {
|
|
EXPECT_TRUE(is_launched_);
|
|
is_launched_ = false;
|
|
DidStop();
|
|
}
|
|
|
|
private:
|
|
const std::vector<std::string> app_ids_;
|
|
const std::string display_name_;
|
|
|
|
std::vector<std::string> namespaces_;
|
|
|
|
int session_id_ = 0;
|
|
bool is_launched_ = false;
|
|
};
|
|
|
|
class ApplicationAgentTest : public ::testing::Test {
|
|
public:
|
|
ApplicationAgentTest() {
|
|
EXPECT_CALL(idle_app_, DidLaunch(_, NotNull()));
|
|
agent_.RegisterApplication(&idle_app_, true);
|
|
Mock::VerifyAndClearExpectations(&idle_app_);
|
|
|
|
ConnectAndDoAuth();
|
|
}
|
|
|
|
~ApplicationAgentTest() override { EXPECT_CALL(idle_app_, DidStop()); }
|
|
|
|
ApplicationAgent* agent() { return &agent_; }
|
|
StrictMock<FakeApplication>* idle_app() { return &idle_app_; }
|
|
|
|
MockCastSocketClient* sender_inbound() {
|
|
return &socket_pair_.mock_peer_client;
|
|
}
|
|
CastSocket* sender_outbound() { return socket_pair_.peer_socket.get(); }
|
|
|
|
// Examines the |message| for the correct source/destination transport IDs and
|
|
// namespace, confirms there is JSON in the payload, and returns parsed JSON
|
|
// (or an empty object if the parse fails).
|
|
static Json::Value ValidateAndParseMessage(const CastMessage& message,
|
|
const std::string& from,
|
|
const std::string& to,
|
|
const std::string& the_namespace) {
|
|
EXPECT_EQ(from, message.source_id());
|
|
EXPECT_EQ(to, message.destination_id());
|
|
EXPECT_EQ(the_namespace, message.namespace_());
|
|
EXPECT_EQ(::cast::channel::CastMessage_PayloadType_STRING,
|
|
message.payload_type());
|
|
EXPECT_FALSE(message.payload_utf8().empty());
|
|
ErrorOr<Json::Value> parsed = json::Parse(message.payload_utf8());
|
|
return parsed.value(Json::Value(Json::objectValue));
|
|
}
|
|
|
|
// Constructs a CastMessage proto for sending via the CastSocket::Send() API.
|
|
static CastMessage MakeCastMessage(const std::string& source_id,
|
|
const std::string& destination_id,
|
|
const std::string& the_namespace,
|
|
const std::string& json) {
|
|
CastMessage message = MakeSimpleUTF8Message(the_namespace, json);
|
|
message.set_source_id(source_id);
|
|
message.set_destination_id(destination_id);
|
|
return message;
|
|
}
|
|
|
|
private:
|
|
// Walk through all the steps to establish a network connection to the
|
|
// ApplicationAgent, and test the plumbing for the auth challenge/reply.
|
|
void ConnectAndDoAuth() {
|
|
static_cast<ReceiverSocketFactory::Client*>(&agent_)->OnConnected(
|
|
nullptr, socket_pair_.local_endpoint, std::move(socket_pair_.socket));
|
|
|
|
// The remote will send the auth challenge message and get a reply.
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([](CastSocket*, CastMessage message) {
|
|
EXPECT_EQ(kAuthNamespace, message.namespace_());
|
|
EXPECT_FALSE(message.payload_binary().empty());
|
|
}));
|
|
const auto result = sender_outbound()->Send(TestAuthChallengeMessage());
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
Mock::VerifyAndClearExpectations(sender_inbound());
|
|
}
|
|
|
|
void TearDown() override {
|
|
// The ApplicationAgent should send a final "no apps running"
|
|
// RECEIVER_STATUS broadcast to the sender at destruction time.
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([](CastSocket*, CastMessage message) {
|
|
constexpr char kExpectedJson[] = R"({
|
|
"requestId":0,
|
|
"type":"RECEIVER_STATUS",
|
|
"status":{
|
|
"userEq":{},
|
|
"volume":{
|
|
"controlType":"attenuation",
|
|
"level":1.0,
|
|
"muted":false,
|
|
"stepInterval":0.05
|
|
}
|
|
}
|
|
})";
|
|
const Json::Value payload = ValidateAndParseMessage(
|
|
message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
}
|
|
|
|
FakeClock clock_{Clock::time_point() + std::chrono::hours(1)};
|
|
FakeTaskRunner task_runner_{&clock_};
|
|
FakeCastSocketPair socket_pair_;
|
|
StrictMock<FakeApplication> idle_app_{"E8C28D3C", "Backdrop"};
|
|
TestCredentialsProvider creds_;
|
|
ApplicationAgent agent_{&task_runner_, &creds_};
|
|
};
|
|
|
|
TEST_F(ApplicationAgentTest, JustConnectsWithoutDoingAnything) {}
|
|
|
|
TEST_F(ApplicationAgentTest, IgnoresGarbageMessages) {
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _)).Times(0);
|
|
|
|
const char* kGarbageStrings[] = {
|
|
"",
|
|
R"(naked text)",
|
|
R"("")",
|
|
R"(123)",
|
|
R"("just a string")",
|
|
R"([])",
|
|
R"({})",
|
|
R"({"type":"GET_STATUS"})", // Note: Missing requestId.
|
|
};
|
|
for (const char* some_string : kGarbageStrings) {
|
|
const auto result = sender_outbound()->Send(
|
|
MakeCastMessage(kPlatformSenderId, kPlatformReceiverId,
|
|
kReceiverNamespace, some_string));
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
}
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, HandlesInvalidCommands) {
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
constexpr char kExpectedJson[] = R"({
|
|
"requestId":3,
|
|
"type":"INVALID_REQUEST",
|
|
"reason":"INVALID_COMMAND"
|
|
})";
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, kPlatformReceiverId,
|
|
kPlatformSenderId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
auto result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":3,
|
|
"type":"FINISH_Q3_OKRS_BY_END_OF_Q3"
|
|
})"));
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, HandlesPings) {
|
|
constexpr int kNumPings = 3;
|
|
|
|
int num_pongs = 0;
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.Times(kNumPings)
|
|
.WillRepeatedly(Invoke([&num_pongs](CastSocket*, CastMessage message) {
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, kPlatformReceiverId,
|
|
kPlatformSenderId, kHeartbeatNamespace);
|
|
EXPECT_EQ(json::Parse(R"({"type":"PONG"})").value(), payload);
|
|
++num_pongs;
|
|
}));
|
|
|
|
const CastMessage message =
|
|
MakeCastMessage(kPlatformSenderId, kPlatformReceiverId,
|
|
kHeartbeatNamespace, R"({"type":"PING"})");
|
|
for (int i = 0; i < kNumPings; ++i) {
|
|
const auto result = sender_outbound()->Send(message);
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
}
|
|
EXPECT_EQ(kNumPings, num_pongs);
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, HandlesGetAppAvailability) {
|
|
// Send the request before any apps have been registered. Expect an
|
|
// "unavailable" response.
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
constexpr char kExpectedJson[] = R"({
|
|
"requestId":548,
|
|
"responseType":"GET_APP_AVAILABILITY",
|
|
"availability":{"1A2B3C4D":"APP_UNAVAILABLE"}
|
|
})";
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, kPlatformReceiverId,
|
|
kPlatformSenderId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
auto result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":548,
|
|
"type":"GET_APP_AVAILABILITY",
|
|
"appId":["1A2B3C4D"]
|
|
})"));
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
|
|
// Register an application.
|
|
FakeApplication some_app("1A2B3C4D", "Something Doer");
|
|
agent()->RegisterApplication(&some_app);
|
|
|
|
// Send another request, and expect the application to be available.
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
constexpr char kExpectedJson[] = R"({
|
|
"requestId":549,
|
|
"responseType":"GET_APP_AVAILABILITY",
|
|
"availability":{"1A2B3C4D":"APP_AVAILABLE"}
|
|
})";
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, kPlatformReceiverId,
|
|
kPlatformSenderId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":549,
|
|
"type":"GET_APP_AVAILABILITY",
|
|
"appId":["1A2B3C4D"]
|
|
})"));
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
|
|
agent()->UnregisterApplication(&some_app);
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, HandlesGetStatus) {
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
constexpr char kExpectedJson[] = R"({
|
|
"requestId":123,
|
|
"type":"RECEIVER_STATUS",
|
|
"status":{
|
|
"applications":[
|
|
{
|
|
// NOTE: These IDs and the displayName come from |idle_app_|.
|
|
"sessionId":"E8C28D3C-9ABC-DEF0-1234-000000000001",
|
|
"appId":"E8C28D3C",
|
|
"universalAppId":"E8C28D3C",
|
|
"displayName":"Backdrop",
|
|
"isIdleScreen":true,
|
|
"launchedFromCloud":false,
|
|
"namespaces":[]
|
|
}
|
|
],
|
|
"userEq":{},
|
|
"volume":{
|
|
"controlType":"attenuation",
|
|
"level":1.0,
|
|
"muted":false,
|
|
"stepInterval":0.05
|
|
}
|
|
}
|
|
})";
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, kPlatformReceiverId,
|
|
kPlatformSenderId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
auto result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":123,
|
|
"type":"GET_STATUS"
|
|
})"));
|
|
ASSERT_TRUE(result.ok()) << result;
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, FailsLaunchRequestWithBadAppID) {
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
constexpr char kExpectedJson[] = R"({
|
|
"requestId":1,
|
|
"type":"LAUNCH_ERROR",
|
|
"reason":"NOT_FOUND"
|
|
})";
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, kPlatformReceiverId,
|
|
kPlatformSenderId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
auto launch_result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":1,
|
|
"type":"LAUNCH",
|
|
"appId":"DEADBEEF"
|
|
})"));
|
|
ASSERT_TRUE(launch_result.ok()) << launch_result;
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, LaunchesApp_PassesMessages_ThenStopsApp) {
|
|
StrictMock<FakeApplication> some_app("1A2B3C4D", "Something Doer");
|
|
constexpr char kAppNamespace[] = "urn:x-cast:com.google.cast.something";
|
|
some_app.SetSupportedNamespaces({std::string(kAppNamespace)});
|
|
agent()->RegisterApplication(&some_app);
|
|
|
|
// Phase 1: Sender sends a LAUNCH request, which causes the idle app to stop
|
|
// and the receiver app to launch. The receiver (ApplicationAgent) broadcasts
|
|
// a RECEIVER_STATUS to indicate the app is running; but the receiver app
|
|
// should not get a copy of that.
|
|
Sequence phase1;
|
|
MessagePort* port_for_app = nullptr;
|
|
EXPECT_CALL(*idle_app(), DidStop()).InSequence(phase1);
|
|
EXPECT_CALL(some_app, DidLaunch(_, NotNull()))
|
|
.InSequence(phase1)
|
|
.WillOnce(Invoke([&](Json::Value params, MessagePort* port) {
|
|
EXPECT_EQ(json::Parse(R"({"a":1,"b":2})").value(), params);
|
|
port_for_app = port;
|
|
port->SetClient(&some_app, some_app.GetSessionId());
|
|
}));
|
|
const std::string kRunningAppReceiverStatus = R"({
|
|
"requestId":0, // Note: 0 for broadcast (no requestor).
|
|
"type":"RECEIVER_STATUS",
|
|
"status":{
|
|
"applications":[
|
|
{
|
|
// NOTE: These IDs and the displayName come from |some_app|.
|
|
"transportId":"1A2B3C4D-9ABC-DEF0-1234-000000000001",
|
|
"sessionId":"1A2B3C4D-9ABC-DEF0-1234-000000000001",
|
|
"appId":"1A2B3C4D",
|
|
"universalAppId":"1A2B3C4D",
|
|
"displayName":"Something Doer",
|
|
"isIdleScreen":false,
|
|
"launchedFromCloud":false,
|
|
"namespaces":[{"name":"urn:x-cast:com.google.cast.something"}]
|
|
}
|
|
],
|
|
"userEq":{},
|
|
"volume":{
|
|
"controlType":"attenuation",
|
|
"level":1.0,
|
|
"muted":false,
|
|
"stepInterval":0.05
|
|
}
|
|
}
|
|
})";
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.InSequence(phase1)
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
const Json::Value payload = ValidateAndParseMessage(
|
|
message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kRunningAppReceiverStatus).value(), payload);
|
|
}));
|
|
auto launch_result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":17,
|
|
"type":"LAUNCH",
|
|
"appId":"1A2B3C4D",
|
|
"appParams":{"a":1,"b":2},
|
|
"language":"en-US",
|
|
"supportedAppTypes":["WEB"]
|
|
})"));
|
|
ASSERT_TRUE(launch_result.ok()) << launch_result;
|
|
Mock::VerifyAndClearExpectations(idle_app());
|
|
Mock::VerifyAndClearExpectations(&some_app);
|
|
Mock::VerifyAndClearExpectations(sender_inbound());
|
|
|
|
// Phase 2: Sender sends a message to the app, and the receiver app sends a
|
|
// reply.
|
|
constexpr char kMessage[] = R"({"type":"FOO","data":"Hello world!"})";
|
|
constexpr char kReplyMessage[] =
|
|
R"({"type":"FOO_REPLY","data":"Hi yourself!"})";
|
|
constexpr char kSenderTransportId[] = "sender-1";
|
|
Sequence phase2;
|
|
EXPECT_CALL(some_app, OnMessage(_, _, _))
|
|
.InSequence(phase2)
|
|
.WillOnce(Invoke([&](const std::string& source_id,
|
|
const std::string& the_namespace,
|
|
const std::string& message) {
|
|
EXPECT_EQ(kSenderTransportId, source_id);
|
|
EXPECT_EQ(kAppNamespace, the_namespace);
|
|
const auto parsed = json::Parse(message);
|
|
EXPECT_TRUE(parsed.is_value()) << parsed.error();
|
|
if (parsed.is_value()) {
|
|
EXPECT_EQ(json::Parse(kMessage).value(), parsed.value());
|
|
if (port_for_app) {
|
|
port_for_app->PostMessage(kSenderTransportId, kAppNamespace,
|
|
kReplyMessage);
|
|
}
|
|
}
|
|
}));
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.InSequence(phase2)
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
const Json::Value payload =
|
|
ValidateAndParseMessage(message, some_app.GetSessionId(),
|
|
kSenderTransportId, kAppNamespace);
|
|
EXPECT_EQ(json::Parse(kReplyMessage).value(), payload);
|
|
}));
|
|
auto message_send_result = sender_outbound()->Send(MakeCastMessage(
|
|
kSenderTransportId, some_app.GetSessionId(), kAppNamespace, kMessage));
|
|
ASSERT_TRUE(message_send_result.ok()) << message_send_result;
|
|
Mock::VerifyAndClearExpectations(&some_app);
|
|
Mock::VerifyAndClearExpectations(sender_inbound());
|
|
|
|
// Phase 3: Sender sends a STOP request, which causes the receiver
|
|
// (ApplicationAgent) to stop the app. Then, the idle app will automatically
|
|
// be re-launched, and a RECEIVER_STATUS broadcast message will notify the
|
|
// sender of that.
|
|
Sequence phase3;
|
|
EXPECT_CALL(some_app, DidStop()).InSequence(phase3);
|
|
EXPECT_CALL(*idle_app(), DidLaunch(_, NotNull())).InSequence(phase3);
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.InSequence(phase3)
|
|
.WillOnce(Invoke([&](CastSocket*, CastMessage message) {
|
|
const std::string kExpectedJson = R"({
|
|
"requestId":0, // Note: 0 for broadcast (no requestor).
|
|
"type":"RECEIVER_STATUS",
|
|
"status":{
|
|
"applications":[
|
|
{
|
|
// NOTE: These IDs and the displayName come from |idle_app_|.
|
|
"sessionId":"E8C28D3C-9ABC-DEF0-1234-000000000002",
|
|
"appId":"E8C28D3C",
|
|
"universalAppId":"E8C28D3C",
|
|
"displayName":"Backdrop",
|
|
"isIdleScreen":true,
|
|
"launchedFromCloud":false,
|
|
"namespaces":[]
|
|
}
|
|
],
|
|
"userEq":{},
|
|
"volume":{
|
|
"controlType":"attenuation",
|
|
"level":1.0,
|
|
"muted":false,
|
|
"stepInterval":0.05
|
|
}
|
|
}
|
|
})";
|
|
const Json::Value payload = ValidateAndParseMessage(
|
|
message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
|
|
EXPECT_EQ(json::Parse(kExpectedJson).value(), payload);
|
|
}));
|
|
auto stop_result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":18,
|
|
"type":"STOP",
|
|
"sessionId":"1A2B3C4D-9ABC-DEF0-1234-000000000001"
|
|
})"));
|
|
ASSERT_TRUE(stop_result.ok()) << stop_result;
|
|
Mock::VerifyAndClearExpectations(idle_app());
|
|
Mock::VerifyAndClearExpectations(&some_app);
|
|
Mock::VerifyAndClearExpectations(sender_inbound());
|
|
|
|
agent()->UnregisterApplication(&some_app);
|
|
}
|
|
|
|
TEST_F(ApplicationAgentTest, AllowsVirtualConnectionsToApp) {
|
|
NiceMock<FakeApplication> some_app("1A2B3C4D", "Something Doer");
|
|
agent()->RegisterApplication(&some_app);
|
|
|
|
// Launch the app, using gMock to simulate an app that calls
|
|
// MessagePort::SetClient() (to permit messaging) and to get the transport ID
|
|
// of the app.
|
|
EXPECT_CALL(*idle_app(), DidStop());
|
|
EXPECT_CALL(some_app, DidLaunch(_, NotNull()))
|
|
.WillOnce(Invoke([&](Json::Value params, MessagePort* port) {
|
|
port->SetClient(&some_app, some_app.GetSessionId());
|
|
}));
|
|
std::string transport_id;
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _))
|
|
.WillRepeatedly(Invoke([&](CastSocket*, CastMessage message) {
|
|
const Json::Value payload = ValidateAndParseMessage(
|
|
message, kPlatformReceiverId, kBroadcastId, kReceiverNamespace);
|
|
if (payload["type"].asString() == "RECEIVER_STATUS") {
|
|
transport_id =
|
|
payload["status"]["applications"][0]["transportId"].asString();
|
|
}
|
|
}));
|
|
auto launch_result = sender_outbound()->Send(MakeCastMessage(
|
|
kPlatformSenderId, kPlatformReceiverId, kReceiverNamespace, R"({
|
|
"requestId":1,
|
|
"type":"LAUNCH",
|
|
"appId":"1A2B3C4D",
|
|
"appParams":{},
|
|
"language":"en-US",
|
|
"supportedAppTypes":["WEB"]
|
|
})"));
|
|
ASSERT_TRUE(launch_result.ok()) << launch_result;
|
|
Mock::VerifyAndClearExpectations(idle_app());
|
|
Mock::VerifyAndClearExpectations(&some_app);
|
|
Mock::VerifyAndClearExpectations(sender_inbound());
|
|
|
|
// Now that the application has launched, check that the policy allows
|
|
// connections to both the ApplicationAgent and the running application.
|
|
auto* const policy =
|
|
static_cast<ConnectionNamespaceHandler::VirtualConnectionPolicy*>(
|
|
agent());
|
|
EXPECT_TRUE(policy->IsConnectionAllowed(
|
|
VirtualConnection{kPlatformReceiverId, "any-sender-12345", 0}));
|
|
ASSERT_FALSE(transport_id.empty());
|
|
EXPECT_TRUE(policy->IsConnectionAllowed(
|
|
VirtualConnection{transport_id, "any-sender-12345", 0}));
|
|
EXPECT_FALSE(policy->IsConnectionAllowed(
|
|
VirtualConnection{"wherever i likes", "any-sender-12345", 0}));
|
|
|
|
// Unregister the app, which will automatically stop it too.
|
|
EXPECT_CALL(some_app, DidStop());
|
|
EXPECT_CALL(*idle_app(), DidLaunch(_, NotNull()));
|
|
EXPECT_CALL(*sender_inbound(), OnMessage(_, _)); // RECEIVER_STATUS update.
|
|
agent()->UnregisterApplication(&some_app);
|
|
|
|
// With the app stopped, check that the policy no longer allows connections to
|
|
// the now-stale |transport_id|.
|
|
EXPECT_TRUE(policy->IsConnectionAllowed(
|
|
VirtualConnection{kPlatformReceiverId, "any-sender-12345", 0}));
|
|
EXPECT_FALSE(policy->IsConnectionAllowed(
|
|
VirtualConnection{transport_id, "any-sender-12345", 0}));
|
|
EXPECT_FALSE(policy->IsConnectionAllowed(
|
|
VirtualConnection{"wherever i likes", "any-sender-12345", 0}));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace cast
|
|
} // namespace openscreen
|