chromium/ios/chrome/browser/web/model/image_fetch/image_fetch_java_script_feature_unittest.mm

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

#import "ios/chrome/browser/web/model/image_fetch/image_fetch_java_script_feature.h"

#import <UIKit/UIKit.h>

#import "base/base64.h"
#import "base/functional/bind.h"
#import "base/test/ios/wait_util.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/web_state.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "url/gurl.h"

using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;

namespace {
// Base64 data of a PNG image.
const char kImageBase64[] =
    "iVBORw0KGgoAAAANSUhEUgAAAIQAAAB+AQMAAADfgyubAAAABlBMVEX+/"
    "v4ODg5o52ONAAABfElEQVRIx+3UO27DMAwAUBoeNBQIe4DAvkLHFDDim/"
    "QMHp2lVpAhY49Ubb2GgFxARZcMqVjKn1SUgl6g4WBAz6QByRQB7vGf48Hy4yWWgUV5IQagvsRC"
    "LG0sRZAhljIInRMphCjSrLEgi5IysLy7SCrOQSFrlnqIpWHZhp1c45mloVh2LH0mOyFfLJ9hJ7"
    "EUJyGnVHily48boiPhlTpm8hZLqEAUchwFpFR1LOqmNFIU6ab1iWwy6YWskHQnhXNcluNEI4Qc"
    "K2SNtE9E0d6kOaWhWBpFByNaY8M5OhWVi2zDDgkzWQG5RAohFqmCVNYip7DoGykG/"
    "SaTXkip0XeZOCFK42WUx/"
    "lwQAF+"
    "2yBPk2wBoSYLLN0kHipAMmGn05eKIPUohV2kYdFAl3Jq1tJDD6FTyKOZryicJ6G5ERUVDmoPMB"
    "At1/"
    "hgx3EwLD+"
    "sJDIQpF0Ougzl7TmIW0aGB2h5UV9vXChv7TQF5tHjpvHzO5CQX72Gcncf3vf4M34AIPQSr4HM1"
    "a4AAAAASUVORK5CYII=";

// Delay of the http response for image.
const int kImageDelayInMs = 1000;

// Request handler of the http server.
std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
    const net::test_server::HttpRequest& request) {
  std::string image_binary;
  EXPECT_TRUE(base::Base64Decode(kImageBase64, &image_binary));

  if (request.GetURL().path() == "/image") {
    auto result = std::make_unique<net::test_server::BasicHttpResponse>();
    result->set_content_type("image/png");
    result->set_content(image_binary);
    result->AddCustomHeader("Access-Control-Allow-Origin", "*");
    return std::move(result);
  }
  if (request.GetURL().path() == "/image_delayed") {
    auto result = std::make_unique<net::test_server::DelayedHttpResponse>(
        base::Milliseconds(kImageDelayInMs));
    result->set_content_type("image/png");
    result->set_content(image_binary);
    result->AddCustomHeader("Access-Control-Allow-Origin", "*");
    return std::move(result);
  }
  return nullptr;
}

// The ID param for calling JS, and will be regained in the message sent back.
const int kCallJavaScriptId = 66666;
}

class ImageFetchJavaScriptFeatureTest
    : public PlatformTest,
      public ImageFetchJavaScriptFeature::Handler {
 protected:
  ImageFetchJavaScriptFeatureTest()
      : web_client_(std::make_unique<web::FakeWebClient>()),
        feature_(base::BindRepeating(
            &ImageFetchJavaScriptFeatureTest::GetHandlerFromTestFixture,
            base::Unretained(this))) {}

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

    browser_state_ = TestChromeBrowserState::Builder().Build();

    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    web_state_->GetView();
    web_state_->SetKeepRenderProcessAlive(true);

    GetWebClient()->SetJavaScriptFeatures({&feature_});

    server_.RegisterDefaultHandler(base::BindRepeating(HandleRequest));
    ASSERT_TRUE(server_.Start());
  }

  ImageFetchJavaScriptFeature::Handler* GetHandlerFromTestFixture(
      web::WebState*) {
    return this;
  }

  web::FakeWebClient* GetWebClient() {
    return static_cast<web::FakeWebClient*>(web_client_.Get());
  }

  void HandleJsSuccess(int call_id,
                       std::string& decoded_data,
                       std::string& from) override {
    EXPECT_FALSE(message_received_);
    message_received_ = true;
    message_id_ = call_id;
    message_from_ = from;
    message_decoded_data_ = decoded_data;
  }

  void HandleJsFailure(int call_id) override {
    EXPECT_FALSE(message_received_);
    message_received_ = true;
    message_id_ = call_id;
  }

  void WaitForResult() {
    ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
      return message_received_;
    }));
  }

  web::WebState* web_state() { return web_state_.get(); }

  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;

  ImageFetchJavaScriptFeature feature_;
  net::EmbeddedTestServer server_;
  bool message_received_ = false;
  int message_id_ = 0;
  std::string message_from_;
  std::string message_decoded_data_;
};

// Tests that __gCrWeb.imageFetch.getImageData works when the image is
// same-domain.
TEST_F(ImageFetchJavaScriptFeatureTest, TestGetSameDomainImageData) {
  const GURL image_url = server_.GetURL("/image");
  const GURL page_url = server_.GetURL("/");
  web::test::LoadHtml([NSString stringWithFormat:@"<html><img src='%s'></html>",
                                                 image_url.spec().c_str()],
                      page_url, web_state());

  feature_.GetImageData(web_state(), kCallJavaScriptId, image_url);
  WaitForResult();

  ASSERT_TRUE(message_received_);
  EXPECT_EQ(kCallJavaScriptId, message_id_);
  // The fetched image data may not equal to original base64 data because of
  // recompression in JavaScript. Therefore decode the returned image data and
  // check if it can be rendered to an image.
  UIImage* image = [UIImage
      imageWithData:[NSData dataWithBytes:message_decoded_data_.c_str()
                                   length:message_decoded_data_.size()]];
  EXPECT_TRUE(image);
  EXPECT_EQ("canvas", message_from_);
}

// Tests that __gCrWeb.imageFetch.getImageData works when the image is
// cross-domain.
TEST_F(ImageFetchJavaScriptFeatureTest, TestGetCrossDomainImageData) {
  const GURL image_url = server_.GetURL("/image");
  // web::test::LoadHtml uses an HTTPS url for webpage as default. Use
  // an HTTP url instead, because XMLHttpRequest with HTTP url sent from HTTPS
  // website is forbidden due to the CORS policy.
  const GURL page_url("http://chrooooome.com");
  web::test::LoadHtml([NSString stringWithFormat:@"<html><img src='%s'></html>",
                                                 image_url.spec().c_str()],
                      page_url, web_state());

  feature_.GetImageData(web_state(), kCallJavaScriptId, image_url);
  WaitForResult();

  ASSERT_TRUE(message_received_);
  EXPECT_EQ(kCallJavaScriptId, message_id_);

  std::string image_binary;
  ASSERT_TRUE(base::Base64Decode(kImageBase64, &image_binary));
  EXPECT_EQ(image_binary, message_decoded_data_);
  EXPECT_EQ("xhr", message_from_);
}

// Tests that __gCrWeb.imageFetch.getImageData fails for timeout when the image
// response is delayed. In this test the image must be cross-domain, otherwise
// image data will be fetched from <img> by <canvas> directly.
TEST_F(ImageFetchJavaScriptFeatureTest, TestGetDelayedImageData) {
  const GURL image_url = server_.GetURL("/image_delayed");
  const GURL page_url("http://chrooooome.com");
  web::test::LoadHtml([NSString stringWithFormat:@"<html><img src='%s'></html>",
                                                 image_url.spec().c_str()],
                      page_url, web_state());

  feature_.GetImageData(web_state(), kCallJavaScriptId, image_url);
  WaitForResult();

  ASSERT_TRUE(message_received_);
  EXPECT_EQ(kCallJavaScriptId, message_id_);
  EXPECT_TRUE(message_decoded_data_.empty());
  EXPECT_TRUE(message_from_.empty());
}