chromium/services/accessibility/features/v8_manager_unittest.cc

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

#include "services/accessibility/features/v8_manager.h"

#include <memory>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "mojo/public/c/system/types.h"
#include "services/accessibility/features/mojo/test/js_test_interface.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8_manager.h"

namespace ax {

class V8ManagerTest : public testing::Test {
 public:
  V8ManagerTest() = default;
  V8ManagerTest(const V8ManagerTest&) = delete;
  V8ManagerTest& operator=(const V8ManagerTest&) = delete;
  ~V8ManagerTest() override = default;

  void SetUp() override { BindingsIsolateHolder::InitializeV8(); }

  std::string GetMojoTestSupportJS() {
    base::FilePath gen_test_data_root;
    base::PathService::Get(base::DIR_GEN_TEST_DATA_ROOT, &gen_test_data_root);
    base::FilePath source_path = gen_test_data_root.Append(FILE_PATH_LITERAL(
        "services/accessibility/features/mojo/test/mojom_test_support.js"));
    std::string script;
    EXPECT_TRUE(ReadFileToString(source_path, &script));
    return script;
  }

  void RunJSMojoTest(const std::string& js_script) {
    base::RunLoop waiter;
    std::unique_ptr<JSTestInterface> test_interface =
        std::make_unique<JSTestInterface>(
            base::BindLambdaForTesting([&waiter](bool success) {
              EXPECT_TRUE(success) << "Mojo JS was not successful";
              waiter.Quit();
            }));
    V8Manager manager;
    manager.AddInterfaceForTest(std::move(test_interface));
    manager.FinishContextSetUp();

    base::RunLoop script_waiter;
    manager.RunScriptForTest(GetMojoTestSupportJS(),
                             script_waiter.QuitClosure());
    // Wait for the script to be executed.
    script_waiter.Run();

    manager.RunScriptForTest(js_script, base::DoNothing());
    // Wait for the test mojom API testComplete method.
    waiter.Run();
  }

 private:
  base::test::TaskEnvironment task_environment_;
};

// Test to execute Javascript that doesn't involve Mojo.
TEST_F(V8ManagerTest, ExecutesSimpleScript) {
  V8Manager manager;
  manager.FinishContextSetUp();
  base::RunLoop script_waiter;
  // Test that this script compiles and runs. That indicates that
  // the atpconsole.log binding was added and that JS works in general.
  manager.RunScriptForTest(R"JS(
    const d = 22;
    var m = 1;
    let y = 1973;
    // By checking there's no error here, we know that V8 bindings
    // can be installed on the context.
    atpconsole.log('Green is the loneliest color');
  )JS",
                           script_waiter.QuitClosure());
  script_waiter.Run();
}

// Sanity check of TextEncoder/TextDecoder.
TEST_F(V8ManagerTest, SanityCheckTextEncoder) {
  V8Manager manager;
  manager.FinishContextSetUp();
  base::RunLoop script_waiter;
  // Test that this script compiles and runs. That indicates there
  // is no issue creating and using TextEncoder/Decoder, but does
  // not verify that the values are as expected.
  manager.RunScriptForTest(R"JS(
    let encoder = new TextEncoder();
    let decoder = new TextDecoder();
    // With contents.
    let encoded = encoder.encode('Hello, world');
    let response = decoder.decode(encoded);
    // Empty.
    encoded = encoder.encode('');
    response = decoder.decode(encoded);
  )JS",
                           script_waiter.QuitClosure());
  script_waiter.Run();
}

// Test that we can do a simple mojom connection. This test will time out
// if the JS remote is not bound to the test_interface.
// TODO(b:262637071) Fails on Fuchsia due to ReadFileToString failing.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_SanityCheckMojoBindings DISABLED_SanityCheckMojoBindings
#else
#define MAYBE_SanityCheckMojoBindings SanityCheckMojoBindings
#endif  // BUILDFLAG(IS_FUCHSIA)
TEST_F(V8ManagerTest, MAYBE_SanityCheckMojoBindings) {
  RunJSMojoTest(R"JS(
    const TestBindingInterface = axtest.mojom.TestBindingInterface;
    const remote = TestBindingInterface.getRemote();
    remote.testComplete(/*success=*/true);)JS");
}

// Test that Mojo constants are defined.
// TODO(b:262637071) Fails on Fuchsia due to ReadFileToString failing.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_CheckMojoConstants DISABLED_CheckMojoConstants
#else
#define MAYBE_CheckMojoConstants CheckMojoConstants
#endif  // BUILDFLAG(IS_FUCHSIA)
TEST_F(V8ManagerTest, MAYBE_CheckMojoConstants) {
  base::RunLoop waiter;
  std::unique_ptr<JSTestInterface> test_interface =
      std::make_unique<JSTestInterface>(
          base::BindLambdaForTesting([&waiter](bool success) {
            EXPECT_TRUE(success) << "Mojo JS was not successful";
            waiter.Quit();
          }));
  V8Manager manager;
  manager.AddInterfaceForTest(std::move(test_interface));
  manager.FinishContextSetUp();

  base::RunLoop script_waiter;
  manager.RunScriptForTest(GetMojoTestSupportJS(), script_waiter.QuitClosure());
  // Wait for the script to be executed.
  script_waiter.Run();

  struct TestCase {
    std::string name;
    MojoResult value;
  };
  const TestCase kTestCases[] = {
      {"RESULT_OK", MOJO_RESULT_OK},
      {"RESULT_CANCELLED", MOJO_RESULT_CANCELLED},
      {"RESULT_UNKNOWN", MOJO_RESULT_UNKNOWN},
      {"RESULT_INVALID_ARGUMENT", MOJO_RESULT_INVALID_ARGUMENT},
      {"RESULT_DEADLINE_EXCEEDED", MOJO_RESULT_DEADLINE_EXCEEDED},
      {"RESULT_NOT_FOUND", MOJO_RESULT_NOT_FOUND},
      {"RESULT_ALREADY_EXISTS", MOJO_RESULT_ALREADY_EXISTS},
      {"RESULT_PERMISSION_DENIED", MOJO_RESULT_PERMISSION_DENIED},
      {"RESULT_RESOURCE_EXHAUSTED", MOJO_RESULT_RESOURCE_EXHAUSTED},
      {"RESULT_FAILED_PRECONDITION", MOJO_RESULT_FAILED_PRECONDITION},
      {"RESULT_ABORTED", MOJO_RESULT_ABORTED},
      {"RESULT_OUT_OF_RANGE", MOJO_RESULT_OUT_OF_RANGE},
      {"RESULT_UNIMPLEMENTED", MOJO_RESULT_UNIMPLEMENTED},
      {"RESULT_INTERNAL", MOJO_RESULT_INTERNAL},
      {"RESULT_UNAVAILABLE", MOJO_RESULT_UNAVAILABLE},
      {"RESULT_DATA_LOSS", MOJO_RESULT_DATA_LOSS},
      {"RESULT_BUSY", MOJO_RESULT_BUSY},
      {"RESULT_SHOULD_WAIT", MOJO_RESULT_SHOULD_WAIT},
  };
  uint32_t index = 0;
  for (auto test : kTestCases) {
    // Make sure the test doesn't skip any MojoResult.
    EXPECT_EQ(test.value, index);
    index++;

    // This will call testComplete(false) if an enum value is missing.
    const std::string script =
        base::StringPrintf(R"JS(
      if (Mojo.%s !== %i) {
        TestBindingInterface.getRemote().testComplete(/*success=*/false);
      }
      )JS",
                           test.name.c_str(), test.value);
    base::RunLoop test_waiter;
    manager.RunScriptForTest(script, test_waiter.QuitClosure());
    test_waiter.Run();
  }
}

// Test to load Mojo bindings and test that an asynchronous callback from JS
// to the remote interface in C++ returns the expected value.
// TODO(b:262637071) Fails on Fuchsia due to ReadFileToString failing.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_MojoBindingsGetsCallback DISABLED_MojoBindingsGetsCallback
#else
#define MAYBE_MojoBindingsGetsCallback MojoBindingsGetsCallback
#endif  // BUILDFLAG(IS_FUCHSIA)
TEST_F(V8ManagerTest, MAYBE_MojoBindingsGetsCallback) {
  RunJSMojoTest(R"JS(
    class TestWrapper {
      constructor() {
        const TestBindingInterface = axtest.mojom.TestBindingInterface;
        this.remote_ = TestBindingInterface.getRemote();
        this.remote_.onConnectionError.addListener(() => {
            console.error('Connection error');
            this.remote_.testComplete(/*success=*/false);
        });
        this.init_();
      }

      async init_() {
        const response = await this.remote_.getTestStruct(41, "RGB");
        // Expect the result struct to have the number passed in plus 1,
        // and the name passed in plus ' rocks'.
        if (response.result.isStructy && response.result.num === 42 &&
            response.result.name === "RGB rocks") {
          this.remote_.testComplete(/*success=*/true);
        } else {
          this.remote_.testComplete(/*success=*/false);
        }
      }
    };
    new TestWrapper();)JS");
}

// Test to see that a PendingReceiver can be send from C++ to
// be bound in Javascript and that it can receive calls from C++.
// TODO(b:262637071) Fails on Fuchsia due to ReadFileToString failing.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_MojoBindingsPendingReceiver DISABLED_MojoBindingsPendingReceiver
#else
#define MAYBE_MojoBindingsPendingReceiver MojoBindingsPendingReceiver
#endif  // BUILDFLAG(IS_FUCHSIA)
TEST_F(V8ManagerTest, MAYBE_MojoBindingsPendingReceiver) {
  RunJSMojoTest(R"JS(
    class TestReceiver {
      constructor(pendingReceiver, callback) {
        this.callback_ = callback;
        this.receiver_ = new axtest.mojom.TestInterfaceReceiver(this);
        this.receiver_.$.bindHandle(pendingReceiver.handle);
      }

      /** @override */
      testMethod(num) {
        this.callback_(num);
      }
    };

    class TestWrapper {
      constructor() {
        const TestBindingInterface = axtest.mojom.TestBindingInterface;
        this.remote_ = TestBindingInterface.getRemote();
        this.remote_.onConnectionError.addListener(() => {
            console.error('Connection error');
            this.remote_.testComplete(/*success=*/false);
        });
        this.init_();
      }

      async init_() {
        let pendingReceiver =
            (await this.remote_.addTestInterface()).interfaceReceiver;
        const receiver = new TestReceiver(pendingReceiver, (num) => {
          if (num == axtest.mojom.TestEnum.kThird) {
            this.remote_.testComplete(/*success=*/true);
          } else {
            this.remote_.testComplete(/*success=*/false);
          }
        });
        await this.remote_.sendEnumToTestInterface(
            axtest.mojom.TestEnum.kThird);
      }
    };
    new TestWrapper();
  )JS");
}

// Test to load Mojo bindings and then disconnect.
// TODO(b:262637071) Fails on Fuchsia due to ReadFileToString failing.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_MojoCancelBindings DISABLED_MojoCancelBindings
#else
#define MAYBE_MojoCancelBindings MojoCancelBindings
#endif  // BUILDFLAG(IS_FUCHSIA)
TEST_F(V8ManagerTest, MAYBE_MojoCancelBindings) {
  RunJSMojoTest(R"JS(
    class TestWrapper {
      constructor() {
        const TestBindingInterface = axtest.mojom.TestBindingInterface;
        this.remote_ = TestBindingInterface.getRemote();
        this.remote_.onConnectionError.addListener(() => {
            this.remote_ = TestBindingInterface.getRemote();
            this.remote_.testComplete(true);
        });
        // Disconnecting from C++ will cause onConnectionError to be
        // called, which is the plumbing we're testing here.
        this.remote_.disconnect();
      }
    };
    new TestWrapper();)JS");
}

// TODO(b:262637071) Fails on Fuchsia due to ReadFileToString failing.
#if BUILDFLAG(IS_FUCHSIA)
#define MAYBE_ExecuteModuleWithImports DISABLED_ExecuteModuleWithImports
#else
#define MAYBE_ExecuteModuleWithImports ExecuteModuleWithImports
#endif  // BUILDFLAG(IS_FUCHSIA)
TEST_F(V8ManagerTest, MAYBE_ExecuteModuleWithImports) {
  base::FilePath gen_test_data_root;
  CHECK(base::PathService::Get(base::DIR_GEN_TEST_DATA_ROOT,
                               &gen_test_data_root));
  base::FilePath file_1_path = gen_test_data_root.Append(FILE_PATH_LITERAL(
      "services/accessibility/features/mojo/test/test_api.test-mojom.m.js"));
  base::File file1(file_1_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  ASSERT_TRUE(file1.IsValid());
  base::FilePath file_2_path = gen_test_data_root.Append(FILE_PATH_LITERAL(
      "services/accessibility/features/mojo/test/module_import.js"));
  base::File file2(file_2_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  ASSERT_TRUE(file2.IsValid());

  base::FilePath file_3_path = gen_test_data_root.Append(
      FILE_PATH_LITERAL("mojo/public/js/bindings.js"));
  base::File file3(file_3_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  ASSERT_TRUE(file3.IsValid());

  base::RunLoop module_waiter;
  std::unique_ptr<JSTestInterface> test_interface =
      std::make_unique<JSTestInterface>(
          base::BindLambdaForTesting([&module_waiter](bool success) {
            EXPECT_TRUE(success) << "Mojo JS was not successful";
            module_waiter.Quit();
          }));
  V8Manager manager;
  manager.AddInterfaceForTest(std::move(test_interface));

  // Important: files are added in fifo order (first file 2 will be evaluated
  // which will request 1 and then 3).
  manager.AddFileForTest(std::move(file2));
  manager.AddFileForTest(std::move(file1));
  manager.AddFileForTest(std::move(file3));
  manager.FinishContextSetUp();
  manager.ExecuteModule(file_2_path, base::DoNothing());
  module_waiter.Run();
}

TEST_F(V8ManagerTest, NormalizesPaths) {
  std::string result = V8Environment::NormalizeRelativePath("a.txt", "b/c");
  EXPECT_EQ(result, "b/c/a.txt");
  result = V8Environment::NormalizeRelativePath("./a.txt", "b/c");
  EXPECT_EQ(result, "b/c/a.txt");
  result = V8Environment::NormalizeRelativePath("../d.txt", "b/c");
  EXPECT_EQ(result, "b/d.txt");

  // Should fail, no base directory to resolve to.
  EXPECT_DEATH(V8Environment::NormalizeRelativePath("e.txt", ""), "");

  // Should fail, base directory ends with '/'.
  EXPECT_DEATH(V8Environment::NormalizeRelativePath("e.txt", "a/"), "");

  // Should fail, relative path references parent of base directory.
  EXPECT_DEATH(V8Environment::NormalizeRelativePath("../../../e.txt", "a/b"),
               "");
}

// TODO(b:313924294): add test to handle missing files.

}  // namespace ax