blob: 3b707bddecfd6d665941b7604318e34359eaacb8 [file] [log] [blame]
// Copyright 2013 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 "content/browser/renderer_host/media/media_stream_ui_proxy.h"
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/run_loop.h"
#include "base/task/post_task.h"
#include "build/build_config.h"
#include "content/browser/frame_host/render_frame_host_delegate.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/media_stream_request.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/test/test_render_frame_host.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"
#include "url/origin.h"
using testing::_;
using testing::Return;
using testing::SaveArg;
namespace content {
namespace {
class MockRenderFrameHostDelegate : public RenderFrameHostDelegate {
public:
void RequestMediaAccessPermission(const MediaStreamRequest& request,
MediaResponseCallback callback) {
return RequestMediaAccessPermission(request, &callback);
}
MOCK_METHOD2(RequestMediaAccessPermission,
void(const MediaStreamRequest& request,
MediaResponseCallback* callback));
MOCK_METHOD3(CheckMediaAccessPermission,
bool(RenderFrameHost* render_frame_host,
const url::Origin& security_origin,
blink::MediaStreamType type));
};
class MockResponseCallback {
public:
MOCK_METHOD2(OnAccessRequestResponse,
void(const blink::MediaStreamDevices& devices,
blink::MediaStreamRequestResult result));
MOCK_METHOD1(OnCheckResponse, void(bool have_access));
};
class MockMediaStreamUI : public MediaStreamUI {
public:
gfx::NativeViewId OnStarted(base::OnceClosure stop,
base::RepeatingClosure source) override {
// gmock cannot handle move-only types:
return MockOnStarted(base::AdaptCallbackForRepeating(std::move(stop)),
source);
}
MOCK_METHOD2(MockOnStarted,
gfx::NativeViewId(base::RepeatingClosure stop,
base::RepeatingClosure source));
};
class MockStopStreamHandler {
public:
MOCK_METHOD0(OnStop, void());
MOCK_METHOD1(OnWindowId, void(gfx::NativeViewId window_id));
};
class MockChangeSourceStreamHandler {
public:
MOCK_METHOD0(OnChangeSource, void());
};
} // namespace
class MediaStreamUIProxyTest : public testing::Test {
public:
MediaStreamUIProxyTest() {
proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_);
}
~MediaStreamUIProxyTest() override {
proxy_.reset();
base::RunLoop().RunUntilIdle();
}
protected:
TestBrowserThreadBundle thread_bundle_;
MockRenderFrameHostDelegate delegate_;
MockResponseCallback response_callback_;
std::unique_ptr<MediaStreamUIProxy> proxy_;
};
MATCHER_P(SameRequest, expected, "") {
return
expected->render_process_id == arg.render_process_id &&
expected->render_frame_id == arg.render_frame_id &&
expected->security_origin == arg.security_origin &&
expected->request_type == arg.request_type &&
expected->requested_audio_device_id == arg.requested_audio_device_id &&
expected->requested_video_device_id == arg.requested_video_device_id &&
expected->audio_type == arg.audio_type &&
expected->video_type == arg.video_type;
}
TEST_F(MediaStreamUIProxyTest, Deny) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
blink::MediaStreamDevices devices;
std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK,
std::unique_ptr<MediaStreamUI>());
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(response.empty());
}
TEST_F(MediaStreamUIProxyTest, AcceptAndStart) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
blink::MediaStreamDevices devices;
devices.push_back(blink::MediaStreamDevice(blink::MEDIA_DEVICE_AUDIO_CAPTURE,
"Mic", "Mic"));
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _)).WillOnce(Return(0));
std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, std::move(ui));
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
proxy_->OnStarted(base::OnceClosure(), base::RepeatingClosure(),
MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
}
// Verify that the proxy can be deleted before the request is processed.
TEST_F(MediaStreamUIProxyTest, DeleteBeforeAccepted) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
proxy_.reset();
blink::MediaStreamDevices devices;
std::unique_ptr<MediaStreamUI> ui;
std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, std::move(ui));
}
TEST_F(MediaStreamUIProxyTest, StopFromUI) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
base::Closure stop_callback;
blink::MediaStreamDevices devices;
devices.push_back(blink::MediaStreamDevice(blink::MEDIA_DEVICE_AUDIO_CAPTURE,
"Mic", "Mic"));
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _))
.WillOnce(testing::DoAll(SaveArg<0>(&stop_callback), Return(0)));
std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, std::move(ui));
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
MockStopStreamHandler stop_handler;
proxy_->OnStarted(base::BindOnce(&MockStopStreamHandler::OnStop,
base::Unretained(&stop_handler)),
base::RepeatingClosure(),
MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(stop_callback.is_null());
EXPECT_CALL(stop_handler, OnStop());
stop_callback.Run();
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaStreamUIProxyTest, WindowIdCallbackCalled) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::MEDIA_NO_SERVICE,
blink::MEDIA_GUM_DESKTOP_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
const int kWindowId = 1;
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _)).WillOnce(Return(kWindowId));
std::move(callback).Run(blink::MediaStreamDevices(), blink::MEDIA_DEVICE_OK,
std::move(ui));
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _));
MockStopStreamHandler handler;
EXPECT_CALL(handler, OnWindowId(kWindowId));
proxy_->OnStarted(base::BindOnce(&MockStopStreamHandler::OnStop,
base::Unretained(&handler)),
base::RepeatingClosure(),
base::BindOnce(&MockStopStreamHandler::OnWindowId,
base::Unretained(&handler)));
base::RunLoop().RunUntilIdle();
}
TEST_F(MediaStreamUIProxyTest, ChangeSourceFromUI) {
auto request = std::make_unique<MediaStreamRequest>(
0, 0, 0, GURL("http://origin/"), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), blink::MEDIA_GUM_DESKTOP_AUDIO_CAPTURE,
blink::MEDIA_GUM_DESKTOP_VIDEO_CAPTURE, false);
MediaStreamRequest* request_ptr = request.get();
proxy_->RequestAccess(
std::move(request),
base::BindOnce(&MockResponseCallback::OnAccessRequestResponse,
base::Unretained(&response_callback_)));
MediaResponseCallback callback;
EXPECT_CALL(delegate_,
RequestMediaAccessPermission(SameRequest(request_ptr), _))
.WillOnce([&](testing::Unused, MediaResponseCallback* cb) {
callback = std::move(*cb);
});
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(callback.is_null());
base::RepeatingClosure source_callback;
blink::MediaStreamDevices devices;
devices.push_back(blink::MediaStreamDevice(
blink::MEDIA_GUM_DESKTOP_VIDEO_CAPTURE, "fake_desktop_video_device",
"Fake Desktop Video Device"));
auto ui = std::make_unique<MockMediaStreamUI>();
EXPECT_CALL(*ui, MockOnStarted(_, _))
.WillOnce(testing::DoAll(SaveArg<1>(&source_callback), Return(0)));
std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, std::move(ui));
blink::MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
MockStopStreamHandler stop_handler;
MockChangeSourceStreamHandler source_handler;
proxy_->OnStarted(
base::BindOnce(&MockStopStreamHandler::OnStop,
base::Unretained(&stop_handler)),
base::BindRepeating(&MockChangeSourceStreamHandler::OnChangeSource,
base::Unretained(&source_handler)),
MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
ASSERT_FALSE(source_callback.is_null());
EXPECT_CALL(source_handler, OnChangeSource());
source_callback.Run();
base::RunLoop().RunUntilIdle();
}
// Basic tests for feature policy checks through the MediaStreamUIProxy. These
// tests are not meant to cover every edge case as the FeaturePolicy class
// itself is tested thoroughly in feature_policy_unittest.cc and in
// render_frame_host_feature_policy_unittest.cc.
class MediaStreamUIProxyFeaturePolicyTest
: public RenderViewHostImplTestHarness {
public:
void SetUp() override {
RenderViewHostImplTestHarness::SetUp();
NavigateAndCommit(GURL("https://example.com"));
}
protected:
// The header policy should only be set once on page load, so we refresh the
// page to simulate that.
void RefreshPageAndSetHeaderPolicy(RenderFrameHost* rfh,
blink::mojom::FeaturePolicyFeature feature,
bool enabled) {
NavigateAndCommit(rfh->GetLastCommittedURL());
std::vector<url::Origin> whitelist;
if (enabled)
whitelist.push_back(rfh->GetLastCommittedOrigin());
RenderFrameHostTester::For(rfh)->SimulateFeaturePolicyHeader(feature,
whitelist);
}
void GetResultForRequest(std::unique_ptr<MediaStreamRequest> request,
blink::MediaStreamDevices* devices_out,
blink::MediaStreamRequestResult* result_out) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::IO},
base::BindOnce(
&MediaStreamUIProxyFeaturePolicyTest::GetResultForRequestOnIOThread,
base::Unretained(this), std::move(request)));
run_loop.Run();
*devices_out = devices_;
*result_out = result_;
}
std::unique_ptr<MediaStreamRequest> CreateRequest(
RenderFrameHost* rfh,
blink::MediaStreamType mic_type,
blink::MediaStreamType cam_type) {
return std::make_unique<MediaStreamRequest>(
rfh->GetProcess()->GetID(), rfh->GetRoutingID(), 0,
rfh->GetLastCommittedURL(), false, blink::MEDIA_GENERATE_STREAM,
std::string(), std::string(), mic_type, cam_type, false);
}
private:
class TestRFHDelegate : public RenderFrameHostDelegate {
void RequestMediaAccessPermission(const MediaStreamRequest& request,
MediaResponseCallback callback) override {
blink::MediaStreamDevices devices;
if (request.audio_type == blink::MEDIA_DEVICE_AUDIO_CAPTURE) {
devices.push_back(blink::MediaStreamDevice(
blink::MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
}
if (request.video_type == blink::MEDIA_DEVICE_VIDEO_CAPTURE) {
devices.push_back(blink::MediaStreamDevice(
blink::MEDIA_DEVICE_VIDEO_CAPTURE, "Camera", "Camera"));
}
auto ui = std::make_unique<MockMediaStreamUI>();
std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, std::move(ui));
}
};
void GetResultForRequestOnIOThread(
std::unique_ptr<MediaStreamRequest> request) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_);
proxy_->RequestAccess(
std::move(request),
base::BindOnce(
&MediaStreamUIProxyFeaturePolicyTest::FinishedGetResultOnIOThread,
base::Unretained(this)));
}
void FinishedGetResultOnIOThread(const blink::MediaStreamDevices& devices,
blink::MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
proxy_.reset();
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&MediaStreamUIProxyFeaturePolicyTest::FinishedGetResult,
base::Unretained(this), devices, result));
}
void FinishedGetResult(const blink::MediaStreamDevices& devices,
blink::MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
devices_ = devices;
result_ = result;
quit_closure_.Run();
}
// These should only be accessed on the UI thread.
blink::MediaStreamDevices devices_;
blink::MediaStreamRequestResult result_;
base::Closure quit_closure_;
// These should only be accessed on the IO thread.
TestRFHDelegate delegate_;
std::unique_ptr<MediaStreamUIProxy> proxy_;
};
TEST_F(MediaStreamUIProxyFeaturePolicyTest, FeaturePolicy) {
blink::MediaStreamDevices devices;
blink::MediaStreamRequestResult result;
// Default FP.
GetResultForRequest(
CreateRequest(main_rfh(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::MEDIA_DEVICE_OK, result);
ASSERT_EQ(2u, devices.size());
EXPECT_EQ(blink::MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type);
EXPECT_EQ(blink::MEDIA_DEVICE_VIDEO_CAPTURE, devices[1].type);
// Mic disabled.
RefreshPageAndSetHeaderPolicy(main_rfh(),
blink::mojom::FeaturePolicyFeature::kMicrophone,
/*enabled=*/false);
GetResultForRequest(
CreateRequest(main_rfh(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::MEDIA_DEVICE_OK, result);
ASSERT_EQ(1u, devices.size());
EXPECT_EQ(blink::MEDIA_DEVICE_VIDEO_CAPTURE, devices[0].type);
// Camera disabled.
RefreshPageAndSetHeaderPolicy(main_rfh(),
blink::mojom::FeaturePolicyFeature::kCamera,
/*enabled=*/false);
GetResultForRequest(
CreateRequest(main_rfh(), blink::MEDIA_DEVICE_AUDIO_CAPTURE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::MEDIA_DEVICE_OK, result);
ASSERT_EQ(1u, devices.size());
EXPECT_EQ(blink::MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type);
// Camera disabled resulting in no devices being returned.
RefreshPageAndSetHeaderPolicy(main_rfh(),
blink::mojom::FeaturePolicyFeature::kCamera,
/*enabled=*/false);
GetResultForRequest(CreateRequest(main_rfh(), blink::MEDIA_NO_SERVICE,
blink::MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(blink::MEDIA_DEVICE_PERMISSION_DENIED, result);
ASSERT_EQ(0u, devices.size());
}
} // namespace content