blob: 0acf2f42c16cfd1eed753924419ba169cff7f5a9 [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/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/frame_host/render_frame_host_delegate.h"
#include "content/public/common/content_features.h"
#include "content/public/test/test_browser_thread.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,
MediaStreamType type));
};
class MockResponseCallback {
public:
MOCK_METHOD2(OnAccessRequestResponse,
void(const MediaStreamDevices& devices,
content::MediaStreamRequestResult result));
MOCK_METHOD1(OnCheckResponse, void(bool have_access));
};
class MockMediaStreamUI : public MediaStreamUI {
public:
MOCK_METHOD1(OnStarted, gfx::NativeViewId(const base::Closure& stop));
};
class MockStopStreamHandler {
public:
MOCK_METHOD0(OnStop, void());
MOCK_METHOD1(OnWindowId, void(gfx::NativeViewId window_id));
};
} // namespace
class MediaStreamUIProxyTest : public testing::Test {
public:
MediaStreamUIProxyTest()
: ui_thread_(BrowserThread::UI, &message_loop_),
io_thread_(BrowserThread::IO, &message_loop_) {
proxy_ = MediaStreamUIProxy::CreateForTests(&delegate_);
}
~MediaStreamUIProxyTest() override {
proxy_.reset();
base::RunLoop().RunUntilIdle();
}
protected:
base::MessageLoop message_loop_;
TestBrowserThread ui_thread_;
TestBrowserThread io_thread_;
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;
}
// These tests are flaky on Linux. https://crbug.com/826483
#if defined(OS_LINUX)
#define MAYBE_DeleteBeforeAccepted DISABLED_DeleteBeforeAccepted
#define MAYBE_Deny DISABLED_Deny
#define MAYBE_AcceptAndStart DISABLED_AcceptAndStart
#define MAYBE_StopFromUI DISABLED_StopFromUI
#define MAYBE_WindowIdCallbackCalled DISABLED_WindowIdCallbackCalled
#else
#define MAYBE_DeleteBeforeAccepted DeleteBeforeAccepted
#define MAYBE_Deny Deny
#define MAYBE_AcceptAndStart AcceptAndStart
#define MAYBE_StopFromUI StopFromUI
#define MAYBE_WindowIdCallbackCalled WindowIdCallbackCalled
#endif
TEST_F(MediaStreamUIProxyTest, MAYBE_Deny) {
std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest(
0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM,
std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE,
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());
MediaStreamDevices devices;
std::move(callback).Run(devices, MEDIA_DEVICE_OK,
std::unique_ptr<MediaStreamUI>());
MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(response.empty());
}
TEST_F(MediaStreamUIProxyTest, MAYBE_AcceptAndStart) {
std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest(
0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM,
std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE,
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());
MediaStreamDevices devices;
devices.push_back(
MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI());
EXPECT_CALL(*ui, OnStarted(_)).WillOnce(Return(0));
std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui));
MediaStreamDevices response;
EXPECT_CALL(response_callback_, OnAccessRequestResponse(_, _))
.WillOnce(SaveArg<0>(&response));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(response.empty());
proxy_->OnStarted(base::Closure(), MediaStreamUIProxy::WindowIdCallback());
base::RunLoop().RunUntilIdle();
}
// Verify that the proxy can be deleted before the request is processed.
TEST_F(MediaStreamUIProxyTest, MAYBE_DeleteBeforeAccepted) {
std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest(
0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM,
std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE,
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();
MediaStreamDevices devices;
std::unique_ptr<MediaStreamUI> ui;
std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui));
}
TEST_F(MediaStreamUIProxyTest, MAYBE_StopFromUI) {
std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest(
0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM,
std::string(), std::string(), MEDIA_DEVICE_AUDIO_CAPTURE,
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;
MediaStreamDevices devices;
devices.push_back(
MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI());
EXPECT_CALL(*ui, OnStarted(_))
.WillOnce(testing::DoAll(SaveArg<0>(&stop_callback), Return(0)));
std::move(callback).Run(devices, MEDIA_DEVICE_OK, std::move(ui));
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)),
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, MAYBE_WindowIdCallbackCalled) {
std::unique_ptr<MediaStreamRequest> request(new MediaStreamRequest(
0, 0, 0, GURL("http://origin/"), false, MEDIA_GENERATE_STREAM,
std::string(), std::string(), MEDIA_NO_SERVICE,
MEDIA_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;
std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI());
EXPECT_CALL(*ui, OnStarted(_)).WillOnce(Return(kWindowId));
std::move(callback).Run(MediaStreamDevices(), 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::BindOnce(&MockStopStreamHandler::OnWindowId,
base::Unretained(&handler)));
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,
MediaStreamDevices* devices_out,
MediaStreamRequestResult* result_out) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::RunLoop run_loop;
quit_closure_ = run_loop.QuitClosure();
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
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,
MediaStreamType mic_type,
MediaStreamType cam_type) {
return std::make_unique<MediaStreamRequest>(
rfh->GetProcess()->GetID(), rfh->GetRoutingID(), 0,
rfh->GetLastCommittedURL(), false, 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 {
MediaStreamDevices devices;
if (request.audio_type == MEDIA_DEVICE_AUDIO_CAPTURE) {
devices.push_back(
MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, "Mic", "Mic"));
}
if (request.video_type == MEDIA_DEVICE_VIDEO_CAPTURE) {
devices.push_back(
MediaStreamDevice(MEDIA_DEVICE_VIDEO_CAPTURE, "Camera", "Camera"));
}
std::unique_ptr<MockMediaStreamUI> ui(new MockMediaStreamUI());
std::move(callback).Run(devices, 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 MediaStreamDevices& devices,
MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
proxy_.reset();
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(&MediaStreamUIProxyFeaturePolicyTest::FinishedGetResult,
base::Unretained(this), devices, result));
}
void FinishedGetResult(const MediaStreamDevices& devices,
MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
devices_ = devices;
result_ = result;
quit_closure_.Run();
}
// These should only be accessed on the UI thread.
MediaStreamDevices devices_;
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) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(features::kUseFeaturePolicyForPermissions);
MediaStreamDevices devices;
MediaStreamRequestResult result;
// Default FP.
GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE,
MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(MEDIA_DEVICE_OK, result);
ASSERT_EQ(2u, devices.size());
EXPECT_EQ(MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type);
EXPECT_EQ(MEDIA_DEVICE_VIDEO_CAPTURE, devices[1].type);
// Mic disabled.
RefreshPageAndSetHeaderPolicy(main_rfh(),
blink::mojom::FeaturePolicyFeature::kMicrophone,
/*enabled=*/false);
GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE,
MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(MEDIA_DEVICE_OK, result);
ASSERT_EQ(1u, devices.size());
EXPECT_EQ(MEDIA_DEVICE_VIDEO_CAPTURE, devices[0].type);
// Camera disabled.
RefreshPageAndSetHeaderPolicy(main_rfh(),
blink::mojom::FeaturePolicyFeature::kCamera,
/*enabled=*/false);
GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE,
MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(MEDIA_DEVICE_OK, result);
ASSERT_EQ(1u, devices.size());
EXPECT_EQ(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(), MEDIA_NO_SERVICE, MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(MEDIA_DEVICE_PERMISSION_DENIED, result);
ASSERT_EQ(0u, devices.size());
// Ensure that the policy is ignored if kUseFeaturePolicyForPermissions is
// disabled.
base::test::ScopedFeatureList empty_feature_list;
empty_feature_list.InitAndDisableFeature(
features::kUseFeaturePolicyForPermissions);
GetResultForRequest(CreateRequest(main_rfh(), MEDIA_DEVICE_AUDIO_CAPTURE,
MEDIA_DEVICE_VIDEO_CAPTURE),
&devices, &result);
EXPECT_EQ(MEDIA_DEVICE_OK, result);
ASSERT_EQ(2u, devices.size());
EXPECT_EQ(MEDIA_DEVICE_AUDIO_CAPTURE, devices[0].type);
EXPECT_EQ(MEDIA_DEVICE_VIDEO_CAPTURE, devices[1].type);
}
} // namespace content