chromium/ios/chrome/browser/permissions/model/media_overrides_javascript_test.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "base/test/ios/wait_util.h"
#import "ios/web/public/test/javascript_test.h"
#import "ios/web/public/test/js_test_util.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "testing/gtest_mac.h"

namespace {
const char kEmptyPageUrl[] = "/blank";
const char kGetUserMediaAudioPageUrl[] = "/getUserMediaAudio";
const char kGetUserMediaAudioVideoPageUrl[] = "/getUserMediaAudioVideo";
const char kGetUserMediaVideoPageUrl[] = "/getUserMediaVideo";

const char kGetUserMediaAudioPageHtml[] =
    "<html><body><script>"
    "navigator.mediaDevices.getUserMedia({ audio : true });"
    "</script></body></html>";
const char kGetUserMediaAudioVideoPageHtml[] =
    "<html><body><script>"
    "navigator.mediaDevices.getUserMedia({ audio : true, video : true });"
    "</script></body></html>";
const char kGetUserMediaVideoPageHtml[] =
    "<html><body><script>"
    "navigator.mediaDevices.getUserMedia({ video : true });"
    "</script></body></html>";

// Provides responses for initial page and destination URLs.
std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
    const net::test_server::HttpRequest& request) {
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_OK);

  if (request.relative_url == kEmptyPageUrl) {
    http_response->set_content("<html><body></body></html>");
  } else if (request.relative_url == kGetUserMediaAudioPageUrl) {
    http_response->set_content(kGetUserMediaAudioPageHtml);
  } else if (request.relative_url == kGetUserMediaAudioVideoPageUrl) {
    http_response->set_content(kGetUserMediaAudioVideoPageHtml);
  } else if (request.relative_url == kGetUserMediaVideoPageUrl) {
    http_response->set_content(kGetUserMediaVideoPageHtml);
  } else {
    return nullptr;
  }
  return std::move(http_response);
}
}  // namespace

static NSString* kMessageHandlerResponseAudioKey = @"audio";
static NSString* kMessageHandlerResponseVideoKey = @"video";

@interface MediaScriptMessageHandler : NSObject <WKScriptMessageHandler>
@property(nonatomic, strong) WKScriptMessage* lastReceivedMessage;
@end

@implementation MediaScriptMessageHandler

- (void)configureForWebView:(WKWebView*)webView {
  [webView.configuration.userContentController
      addScriptMessageHandler:self
                         name:@"MediaAPIAccessedHandler"];
}

- (void)userContentController:(WKUserContentController*)userContentController
      didReceiveScriptMessage:(WKScriptMessage*)message {
  self.lastReceivedMessage = message;
}

@end

// Test fixture for media_overrides.ts.
class MediaOverridesJavaScriptTest : public web::JavascriptTest {
 protected:
  MediaOverridesJavaScriptTest()
      : server_(net::EmbeddedTestServer::TYPE_HTTP),
        message_handler_([[MediaScriptMessageHandler alloc] init]) {}
  ~MediaOverridesJavaScriptTest() override {}

  void SetUp() override {
    JavascriptTest::SetUp();

    AddUserScript(@"media_overrides");

    server_.RegisterRequestHandler(base::BindRepeating(&StandardResponse));
    ASSERT_TRUE(server_.Start());

    [message_handler_ configureForWebView:web_view()];
  }

  const net::EmbeddedTestServer& server() { return server_; }

  MediaScriptMessageHandler* message_handler() { return message_handler_; }

 private:
  net::EmbeddedTestServer server_;
  MediaScriptMessageHandler* message_handler_;
};

// Tests that `navigator.mediaDevices` API is not added for insecure pages where
// the API does not already exist. (Pages loaded with LoadHtml are not
// considered secure within WebKit.)
TEST_F(MediaOverridesJavaScriptTest, MediaAPINotAddedForInsecureContexts) {
  ASSERT_TRUE(LoadHtml(@"<html><head></head><body></body></html>"));

  id result = web::test::ExecuteJavaScript(web_view(),
                                           @"typeof navigator.mediaDevices");
  EXPECT_NSEQ(@"undefined", result);
}

// Tests that the original `getMediaDevices` method is still called by checking
// that it returns a promise.
TEST_F(MediaOverridesJavaScriptTest, MediaOverrideCallsOriginal) {
  ASSERT_TRUE(LoadUrl(server().GetURL(kEmptyPageUrl)));

  id result = web::test::ExecuteJavaScript(
      web_view(), @"const promise = navigator.mediaDevices.getUserMedia({ "
                  @"audio : true }); typeof promise.then;");
  EXPECT_NSEQ(@"function", result);
}

// Tests that the correct message is received for an audio media request.
TEST_F(MediaOverridesJavaScriptTest, WebKitAudioMessageReceived) {
  GURL URL = server().GetURL(kGetUserMediaAudioPageUrl);
  ASSERT_TRUE(LoadUrl(URL));

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForPageLoadTimeout, ^{
        return message_handler().lastReceivedMessage != nil;
      }));
  NSDictionary* body = message_handler().lastReceivedMessage.body;
  NSArray* allKeys = body.allKeys;
  EXPECT_EQ(2ul, allKeys.count);
  EXPECT_TRUE([allKeys containsObject:kMessageHandlerResponseAudioKey]);
  EXPECT_TRUE([allKeys containsObject:kMessageHandlerResponseVideoKey]);

  EXPECT_TRUE([body[kMessageHandlerResponseAudioKey] boolValue]);
  EXPECT_FALSE([body[kMessageHandlerResponseVideoKey] boolValue]);
}

// Tests that the correct message is received for an audio and video media
// request.
TEST_F(MediaOverridesJavaScriptTest, WebKitAudioVideoMessageReceived) {
  GURL URL = server().GetURL(kGetUserMediaAudioVideoPageUrl);
  ASSERT_TRUE(LoadUrl(URL));

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForPageLoadTimeout, ^{
        return message_handler().lastReceivedMessage != nil;
      }));
  NSDictionary* body = message_handler().lastReceivedMessage.body;
  NSArray* allKeys = body.allKeys;
  EXPECT_EQ(2ul, allKeys.count);
  EXPECT_TRUE([allKeys containsObject:kMessageHandlerResponseAudioKey]);
  EXPECT_TRUE([allKeys containsObject:kMessageHandlerResponseVideoKey]);

  EXPECT_TRUE([body[kMessageHandlerResponseAudioKey] boolValue]);
  EXPECT_TRUE([body[kMessageHandlerResponseVideoKey] boolValue]);
}

// Tests that the correct message is received for a video media request.
TEST_F(MediaOverridesJavaScriptTest, WebKitVideoMessageReceived) {
  GURL URL = server().GetURL(kGetUserMediaVideoPageUrl);
  ASSERT_TRUE(LoadUrl(URL));

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForPageLoadTimeout, ^{
        return message_handler().lastReceivedMessage != nil;
      }));
  NSDictionary* body = message_handler().lastReceivedMessage.body;
  NSArray* allKeys = body.allKeys;
  EXPECT_EQ(2ul, allKeys.count);
  EXPECT_TRUE([allKeys containsObject:kMessageHandlerResponseAudioKey]);
  EXPECT_TRUE([allKeys containsObject:kMessageHandlerResponseVideoKey]);

  EXPECT_FALSE([body[kMessageHandlerResponseAudioKey] boolValue]);
  EXPECT_TRUE([body[kMessageHandlerResponseVideoKey] boolValue]);
}