blob: 9e4494a232fc50e06b7ffbc90bae6e32a75e1efb [file] [log] [blame]
// Copyright 2018 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/media/router/providers/cast/cast_session_tracker.h"
#include "base/test/values_test_util.h"
#include "chrome/browser/media/router/test/test_helper.h"
#include "chrome/common/media_router/test/test_helper.h"
#include "components/cast_channel/cast_test_util.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::test::IsJson;
using base::test::ParseJson;
using testing::_;
using testing::ByRef;
using testing::Eq;
namespace media_router {
namespace {
constexpr char kReceiverStatus[] = R"({
"status": {
"applications": [{
"appId": "ABCDEFGH",
"displayName": "App display name",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "theSessionId",
"statusText":"App status",
"transportId":"theTransportId"
}]
}
})";
// Receiver status for the backdrop (idle) app.
constexpr char kIdleReceiverStatus[] = R"({
"status": {
"applications": [{
"appId": "E8C28D3C",
"displayName": "Backdrop",
"namespaces": [
{"name": "urn:x-cast:com.google.cast.media"},
{"name": "urn:x-cast:com.google.foo"}
],
"sessionId": "theSessionId",
"statusText":"App status",
"transportId":"theTransportId"
}]
}
})";
} // namespace
class MockCastSessionObserver : public CastSessionTracker::Observer {
public:
MockCastSessionObserver() = default;
~MockCastSessionObserver() override = default;
MOCK_METHOD2(OnSessionAddedOrUpdated,
void(const MediaSinkInternal& sink, const CastSession& session));
MOCK_METHOD1(OnSessionRemoved, void(const MediaSinkInternal& sink));
MOCK_METHOD3(OnMediaStatusUpdated,
void(const MediaSinkInternal& sink,
const base::Value& media_status,
base::Optional<int> request_id));
};
class CastSessionTrackerTest : public testing::Test {
public:
CastSessionTrackerTest()
: socket_service_(base::CreateSingleThreadTaskRunnerWithTraits(
{content::BrowserThread::UI})),
message_handler_(&socket_service_),
session_tracker_(&media_sink_service_,
&message_handler_,
socket_service_.task_runner()) {
base::RunLoop().RunUntilIdle();
}
void SetUp() override { session_tracker_.AddObserver(&observer_); }
void TearDown() override { session_tracker_.RemoveObserver(&observer_); }
void AddSinkAndSendReceiverStatusResponse() {
EXPECT_CALL(message_handler_,
RequestReceiverStatus(sink_.cast_data().cast_channel_id));
media_sink_service_.AddOrUpdateSink(sink_);
EXPECT_CALL(observer_, OnSessionAddedOrUpdated(sink_, _));
session_tracker_.OnInternalMessage(
sink_.cast_data().cast_channel_id,
cast_channel::InternalMessage(
cast_channel::CastMessageType::kReceiverStatus,
std::move(*ParseJson(kReceiverStatus))));
session_ = session_tracker_.GetSessions().begin()->second.get();
ASSERT_TRUE(session_);
}
protected:
content::TestBrowserThreadBundle thread_bundle_;
cast_channel::MockCastSocketService socket_service_;
cast_channel::MockCastMessageHandler message_handler_;
TestMediaSinkService media_sink_service_;
CastSessionTracker session_tracker_;
MockCastSessionObserver observer_;
MediaSinkInternal sink_ = CreateCastSink(1);
CastSession* session_;
};
TEST_F(CastSessionTrackerTest, QueryReceiverOnSinkAdded) {
AddSinkAndSendReceiverStatusResponse();
// Receiver status is sent again when sinks is updated.
sink_.cast_data().cast_channel_id = 2;
EXPECT_CALL(message_handler_,
RequestReceiverStatus(sink_.cast_data().cast_channel_id));
media_sink_service_.AddOrUpdateSink(sink_);
}
TEST_F(CastSessionTrackerTest, RemoveSessionOnSinkRemoved) {
AddSinkAndSendReceiverStatusResponse();
EXPECT_CALL(observer_, OnSessionRemoved(sink_));
media_sink_service_.RemoveSink(sink_);
}
TEST_F(CastSessionTrackerTest, RemoveSession) {
AddSinkAndSendReceiverStatusResponse();
EXPECT_CALL(observer_, OnSessionRemoved(sink_));
session_tracker_.OnInternalMessage(
sink_.cast_data().cast_channel_id,
cast_channel::InternalMessage(
cast_channel::CastMessageType::kReceiverStatus,
std::move(*ParseJson(kIdleReceiverStatus))));
}
TEST_F(CastSessionTrackerTest, GetSessions) {
EXPECT_TRUE(session_tracker_.GetSessions().empty());
AddSinkAndSendReceiverStatusResponse();
const auto& sessions = session_tracker_.GetSessions();
EXPECT_EQ(1u, sessions.size());
auto it = sessions.find(sink_.sink().id());
ASSERT_TRUE(it != sessions.end());
EXPECT_EQ("theSessionId", it->second->session_id());
EXPECT_TRUE(session_tracker_.GetSessionById("theSessionId"));
}
TEST_F(CastSessionTrackerTest, HandleMediaStatusMessageBasic) {
AddSinkAndSendReceiverStatusResponse();
// Expect that:
//
// - Any 'status' entries with 'playerState' equal to "IDLE" are filtered out.
//
// - The session ID is copied into the output message and all values in in the
// 'status' list.
//
// - A request ID is not required.
//
// - A 'supportedMediaRequests' field whose value is zero in the 'status'
// objects is converted to an empty list.
//
EXPECT_CALL(observer_, OnMediaStatusUpdated(sink_, IsJson(R"({
"sessionId": "theSessionId",
"status": [{
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": [],
},
],
})"),
base::Optional<int>()));
// This should call session_tracker_.HandleMediaStatusMessage(...).
session_tracker_.OnInternalMessage(
sink_.cast_data().cast_channel_id,
cast_channel::InternalMessage(cast_channel::CastMessageType::kMediaStatus,
std::move(*ParseJson(R"({
"status": [{
"playerState": "anything but IDLE",
"supportedMediaRequests": 0,
}, {
"playerState": "IDLE",
},
],
})"))));
// Check that the stored media value is the same as the 'status' field in the
// outgoing message.
EXPECT_THAT(*session_->value().FindKey("media"), IsJson(R"([{
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": [],
}])"));
}
TEST_F(CastSessionTrackerTest, HandleMediaStatusMessageFancy) {
AddSinkAndSendReceiverStatusResponse();
// Expect that:
//
// - Any 'status' entries with 'playerState' equal to "IDLE" are filtered out.
//
// - The session ID is copied into the output message and all values in in the
// 'status' list.
//
// - The request ID is copied into the output message and passed as a separate
// parameters to OnMediaStatusUpdated().
//
// - A nonzero numeric 'supportedMediaRequests' field in the 'status' objects
// is converted to a non-empty list.
//
// - Extra fields are preserved in the message and the status objects.
//
EXPECT_CALL(observer_, OnMediaStatusUpdated(sink_, IsJson(R"({
"requestId": 12345,
"sessionId": "theSessionId",
"status": [{
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": ["pause"],
"xyzzy": "xyzzyValue1",
},
],
"xyzzy": "xyzzyValue2",
})"),
base::make_optional(12345)));
// This should call session_tracker_.HandleMediaStatusMessage(...).
session_tracker_.OnInternalMessage(
sink_.cast_data().cast_channel_id,
cast_channel::InternalMessage(cast_channel::CastMessageType::kMediaStatus,
std::move(*ParseJson(R"({
"requestId": 12345,
"status": [{
"playerState": "anything but IDLE",
"supportedMediaRequests": 1,
"xyzzy": "xyzzyValue1",
}, {
"playerState": "IDLE",
},
],
"xyzzy": "xyzzyValue2",
})"))));
// Check that the stored media value is the same as the 'status' field in the
// outgoing message.
EXPECT_THAT(*session_->value().FindKey("media"), IsJson(R"([{
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": ["pause"],
"xyzzy": "xyzzyValue1",
}])"));
}
TEST_F(CastSessionTrackerTest, CopySavedMediaFieldsToMediaList) {
AddSinkAndSendReceiverStatusResponse();
// Add media status information to the session with mediaSessionId = 345.
EXPECT_CALL(observer_, OnMediaStatusUpdated(sink_, _, _));
session_tracker_.OnInternalMessage(
sink_.cast_data().cast_channel_id,
cast_channel::InternalMessage(cast_channel::CastMessageType::kMediaStatus,
std::move(*ParseJson(R"({
"status": [{
"media": "theMedia",
"mediaSessionId": 345,
"playerState": "anything but IDLE",
"supportedMediaRequests": 0,
"xyzzy": "xyzzy1",
},
],
})"))));
// Check that the stored media value is what we expected.
ASSERT_THAT(*session_->value().FindKey("media"), IsJson(R"([{
"mediaSessionId": 345,
"media": "theMedia",
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": [],
"xyzzy": "xyzzy1",
}])"));
// Not strictly needed, but makes this test easier to debug.
testing::Mock::VerifyAndClear(&observer_);
// Expect the outgoing status message to have a 'media' field filled in from
// the previously stored value.
EXPECT_CALL(observer_, OnMediaStatusUpdated(sink_, IsJson(R"({
"sessionId": "theSessionId",
"status": [{
"media": "theMedia",
"mediaSessionId": 345,
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": [],
"xyzzy": "xyzzy2",
},
],
})"),
_));
// Receive a message referring to the previously stored mediaSessionId with a
// missing 'media' field. This tests the logic in the
// CopySavedMediaFieldsToMediaList() method.
session_tracker_.OnInternalMessage(
sink_.cast_data().cast_channel_id,
cast_channel::InternalMessage(cast_channel::CastMessageType::kMediaStatus,
std::move(*ParseJson(R"({
"status": [{
"mediaSessionId": 345,
"playerState": "anything but IDLE",
"supportedMediaRequests": 0,
"xyzzy": "xyzzy2",
},
],
})"))));
// Check that the stored media value is the same as the 'status' field in the
// outgoing message.
EXPECT_THAT(*session_->value().FindKey("media"), IsJson(R"([{
"media": "theMedia",
"mediaSessionId": 345,
"playerState": "anything but IDLE",
"sessionId": "theSessionId",
"supportedMediaRequests": [],
"xyzzy": "xyzzy2",
}])"));
}
} // namespace media_router