text: // Copyright 2017 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 "extensions/renderer/bindings/api_binding_js_util.h"
  
  #include "base/bind.h"
  #include "base/optional.h"
  #include "extensions/renderer/bindings/api_binding_test_util.h"
  #include "extensions/renderer/bindings/api_bindings_system.h"
  #include "extensions/renderer/bindings/api_bindings_system_unittest.h"
  #include "extensions/renderer/bindings/api_invocation_errors.h"
  #include "gin/arguments.h"
  #include "gin/handle.h"
  #include "testing/gmock/include/gmock/gmock.h"
  
  namespace extensions {
  
  namespace {
  
  // Calls handleException on |obj|, which is presumed to be the JS binding util.
  const char kHandleException[] =
      "try {\n"
      "  throw new Error('some error');\n"
      "} catch (e) {\n"
      "  obj.handleException('handled', e);\n"
      "}";
  
  }  // namespace
  
  class APIBindingJSUtilUnittest : public APIBindingsSystemTest {
   protected:
    APIBindingJSUtilUnittest() {}
    ~APIBindingJSUtilUnittest() override {}
  
    gin::Handle<APIBindingJSUtil> CreateUtil() {
      return gin::CreateHandle(
          isolate(),
          new APIBindingJSUtil(bindings_system()->type_reference_map(),
                               bindings_system()->request_handler(),
                               bindings_system()->event_handler(),
                               bindings_system()->exception_handler()));
    }
  
    v8::Local<v8::Object> GetLastErrorParent(
        v8::Local<v8::Context> context,
        v8::Local<v8::Object>* secondary_parent) override {
      return context->Global();
    }
  
    void AddConsoleError(v8::Local<v8::Context> context,
                         const std::string& error) override {
      console_errors_.push_back(error);
    }
  
    std::string GetExposedError(v8::Local<v8::Context> context) {
      v8::Local<v8::Value> last_error =
          GetPropertyFromObject(context->Global(), context, "lastError");
  
      // Use ADD_FAILURE() to avoid messing up the return type with ASSERT.
      if (last_error.IsEmpty()) {
        ADD_FAILURE();
        return std::string();
      }
      if (!last_error->IsObject() && !last_error->IsUndefined()) {
        ADD_FAILURE();
        return std::string();
      }
  
      if (last_error->IsUndefined())
        return "[undefined]";
      return GetStringPropertyFromObject(last_error.As<v8::Object>(), context,
                                         "message");
    }
  
    APILastError* last_error() {
      return bindings_system()->request_handler()->last_error();
    }
  
    const std::vector<std::string>& console_errors() const {
      return console_errors_;
    }
  
   private:
    std::vector<std::string> console_errors_;
  
    DISALLOW_COPY_AND_ASSIGN(APIBindingJSUtilUnittest);
  };
  
  TEST_F(APIBindingJSUtilUnittest, TestSetLastError) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    EXPECT_FALSE(last_error()->HasError(context));
    EXPECT_EQ("[undefined]", GetExposedError(context));
    const char kSetLastError[] = "obj.setLastError('a last error');";
    CallFunctionOnObject(context, v8_util, kSetLastError);
    EXPECT_TRUE(last_error()->HasError(context));
    EXPECT_EQ("\"a last error\"", GetExposedError(context));
  
    CallFunctionOnObject(context, v8_util,
                         "obj.setLastError('a new last error')");
    EXPECT_TRUE(last_error()->HasError(context));
    EXPECT_EQ("\"a new last error\"", GetExposedError(context));
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestHasLastError) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    EXPECT_FALSE(last_error()->HasError(context));
    EXPECT_EQ("[undefined]", GetExposedError(context));
    const char kHasLastError[] = "return obj.hasLastError();";
    v8::Local<v8::Value> has_error =
        CallFunctionOnObject(context, v8_util, kHasLastError);
    EXPECT_EQ("false", V8ToString(has_error, context));
  
    last_error()->SetError(context, "an error");
    EXPECT_TRUE(last_error()->HasError(context));
    EXPECT_EQ("\"an error\"", GetExposedError(context));
    has_error = CallFunctionOnObject(context, v8_util, kHasLastError);
    EXPECT_EQ("true", V8ToString(has_error, context));
  
    last_error()->ClearError(context, false);
    EXPECT_FALSE(last_error()->HasError(context));
    EXPECT_EQ("[undefined]", GetExposedError(context));
    has_error = CallFunctionOnObject(context, v8_util, kHasLastError);
    EXPECT_EQ("false", V8ToString(has_error, context));
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestRunWithLastError) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    EXPECT_FALSE(last_error()->HasError(context));
    EXPECT_EQ("[undefined]", GetExposedError(context));
  
    const char kRunWithLastError[] =
        "obj.runCallbackWithLastError('last error', function() {\n"
        "  this.exposedLastError =\n"
        "      this.lastError ? this.lastError.message : 'undefined';\n"
        "}, [1, 'foo']);";
    CallFunctionOnObject(context, v8_util, kRunWithLastError);
  
    EXPECT_FALSE(last_error()->HasError(context));
    EXPECT_EQ("[undefined]", GetExposedError(context));
    EXPECT_EQ("\"last error\"",
              GetStringPropertyFromObject(context->Global(), context,
                                          "exposedLastError"));
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestSendRequestWithOptions) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    const char kSendRequestWithNoOptions[] =
        "obj.sendRequest('alpha.functionWithCallback',\n"
        "                ['someString', function() {}], undefined, undefined);";
    CallFunctionOnObject(context, v8_util, kSendRequestWithNoOptions);
    ASSERT_TRUE(last_request());
    EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
    EXPECT_EQ("[\"someString\"]", ValueToString(*last_request()->arguments));
    EXPECT_EQ(binding::RequestThread::UI, last_request()->thread);
    reset_last_request();
  
    const char kSendRequestForIOThread[] =
        "obj.sendRequest('alpha.functionWithCallback',\n"
        "                ['someOtherString', function() {}], undefined,\n"
        "                {__proto__: null, forIOThread: true});";
    CallFunctionOnObject(context, v8_util, kSendRequestForIOThread);
    ASSERT_TRUE(last_request());
    EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
    EXPECT_EQ("[\"someOtherString\"]", ValueToString(*last_request()->arguments));
    EXPECT_EQ(binding::RequestThread::IO, last_request()->thread);
    reset_last_request();
  
    const char kSendRequestForUIThread[] =
        "obj.sendRequest('alpha.functionWithCallback',\n"
        "                ['someOtherString', function() {}], undefined,\n"
        "                {__proto__: null, forIOThread: false});";
    CallFunctionOnObject(context, v8_util, kSendRequestForUIThread);
    ASSERT_TRUE(last_request());
    EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
    EXPECT_EQ("[\"someOtherString\"]", ValueToString(*last_request()->arguments));
    EXPECT_EQ(binding::RequestThread::UI, last_request()->thread);
    reset_last_request();
  
    const char kSendRequestWithCustomCallback[] =
        R"(obj.sendRequest(
               'alpha.functionWithCallback',
               ['stringy', function() {}], undefined,
               {
                 __proto__: null,
                 customCallback: function() {
                   this.callbackCalled = true;
                 },
               });)";
    CallFunctionOnObject(context, v8_util, kSendRequestWithCustomCallback);
    ASSERT_TRUE(last_request());
    EXPECT_EQ("alpha.functionWithCallback", last_request()->method_name);
    EXPECT_EQ("[\"stringy\"]", ValueToString(*last_request()->arguments));
    EXPECT_EQ(binding::RequestThread::UI, last_request()->thread);
    bindings_system()->CompleteRequest(last_request()->request_id,
                                       base::ListValue(), std::string());
    EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
                                                  "callbackCalled"));
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestCallHandleException) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    ASSERT_TRUE(console_errors().empty());
    CallFunctionOnObject(context, v8_util, kHandleException);
    EXPECT_THAT(console_errors(),
                testing::ElementsAre("handled: Error: some error"));
  
    const char kHandleTrickyException[] =
        "try {\n"
        "  throw { toString: function() { throw new Error('hahaha'); } };\n"
        "} catch (e) {\n"
        "  obj.handleException('handled again', e);\n"
        "}\n";
    CallFunctionOnObject(context, v8_util, kHandleTrickyException);
    EXPECT_THAT(
        console_errors(),
        testing::ElementsAre("handled: Error: some error",
                             "handled again: (failed to get error message)"));
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestSetExceptionHandler) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    struct ErrorInfo {
      std::string full_message;
      std::string exception_message;
    };
  
    auto custom_handler = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
      gin::Arguments arguments(info);
      std::string full_message;
      ASSERT_TRUE(arguments.GetNext(&full_message));
      v8::Local<v8::Object> error_object;
      ASSERT_TRUE(arguments.GetNext(&error_object));
  
      ASSERT_TRUE(info.Data()->IsExternal());
      ErrorInfo* error_out =
          static_cast<ErrorInfo*>(info.Data().As<v8::External>()->Value());
      error_out->full_message = full_message;
      error_out->exception_message = GetStringPropertyFromObject(
          error_object, arguments.GetHolderCreationContext(), "message");
    };
  
    ErrorInfo error_info;
    v8::Local<v8::Function> v8_handler =
        v8::Function::New(context, custom_handler,
                          v8::External::New(isolate(), &error_info))
            .ToLocalChecked();
    v8::Local<v8::Function> add_handler = FunctionFromString(
        context,
        "(function(util, handler) { util.setExceptionHandler(handler); })");
    v8::Local<v8::Value> args[] = {v8_util, v8_handler};
    RunFunction(add_handler, context, arraysize(args), args);
  
    CallFunctionOnObject(context, v8_util, kHandleException);
    // The error should not have been reported to the console since we have a
    // cusotm handler.
    EXPECT_TRUE(console_errors().empty());
    EXPECT_EQ("handled: Error: some error", error_info.full_message);
    EXPECT_EQ("\"some error\"", error_info.exception_message);
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestValidateType) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    auto call_validate_type = [context, v8_util](
                                  const char* function,
                                  base::Optional<std::string> expected_error) {
      v8::Local<v8::Function> v8_function = FunctionFromString(context, function);
      v8::Local<v8::Value> args[] = {v8_util};
      if (expected_error) {
        RunFunctionAndExpectError(v8_function, context, arraysize(args), args,
                                  *expected_error);
      } else {
        RunFunction(v8_function, context, arraysize(args), args);
      }
    };
  
    // Test a case that should succeed (a valid value).
    call_validate_type(
        R"((function(util) {
             util.validateType('alpha.objRef', {prop1: 'hello'});
           }))",
        base::nullopt);
  
    // Test a failing case (prop1 is supposed to be a string).
    std::string expected_error =
        "Uncaught TypeError: " +
        api_errors::PropertyError(
            "prop1", api_errors::InvalidType(api_errors::kTypeString,
                                             api_errors::kTypeInteger));
    call_validate_type(
        R"((function(util) {
             util.validateType('alpha.objRef', {prop1: 2});
           }))",
        expected_error);
  }
  
  TEST_F(APIBindingJSUtilUnittest, TestValidateCustomSignature) {
    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();
  
    gin::Handle<APIBindingJSUtil> util = CreateUtil();
    v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
  
    constexpr char kSignatureName[] = "custom_signature";
    EXPECT_FALSE(bindings_system()->type_reference_map()->GetCustomSignature(
        kSignatureName));
  
    {
      constexpr char kAddSignature[] =
          R"((function(util) {
               util.addCustomSignature(
                   'custom_signature',
                   [{type: 'integer'}, {'type': 'string'}]);
             }))";
      v8::Local<v8::Function> add_signature =
          FunctionFromString(context, kAddSignature);
      v8::Local<v8::Value> args[] = {v8_util};
      RunFunction(add_signature, context, arraysize(args), args);
    }
  
    EXPECT_TRUE(bindings_system()->type_reference_map()->GetCustomSignature(
        kSignatureName));
  
    auto call_validate_signature =
        [context, v8_util](const char* function,
                           base::Optional<std::string> expected_error) {
          v8::Local<v8::Function> v8_function =
              FunctionFromString(context, function);
          v8::Local<v8::Value> args[] = {v8_util};
          if (expected_error) {
            RunFunctionAndExpectError(v8_function, context, arraysize(args), args,
                                      *expected_error);
          } else {
            RunFunction(v8_function, context, arraysize(args), args);
          }
        };
  
    // Test a case that should succeed (a valid value).
    call_validate_signature(
        R"((function(util) {
             util.validateCustomSignature('custom_signature', [1, 'foo']);
           }))",
        base::nullopt);
  
    // Test a failing case (prop1 is supposed to be a string).
    std::string expected_error =
        "Uncaught TypeError: " + api_errors::NoMatchingSignature();
    call_validate_signature(
        R"((function(util) {
             util.validateCustomSignature('custom_signature', [1, 2]);
           }))",
        expected_error);
  }
  
  }  // namespace extensions
  