blob: 6622d7dc3035e42a9a1407a67eb19f5bf64ef4f3 [file] [log] [blame]
// Copyright 2016 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/argument_spec.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/values.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_invocation_errors.h"
#include "extensions/renderer/bindings/api_type_reference_map.h"
#include "extensions/renderer/bindings/argument_spec_builder.h"
#include "gin/converter.h"
#include "gin/dictionary.h"
#include "gin/public/isolate_holder.h"
#include "gin/test/v8_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8.h"
namespace extensions {
using V8Validator = base::OnceCallback<void(v8::Local<v8::Value>)>;
class ArgumentSpecUnitTest : public gin::V8Test {
protected:
ArgumentSpecUnitTest()
: type_refs_(APITypeReferenceMap::InitializeTypeCallback()) {}
~ArgumentSpecUnitTest() override {}
enum class TestResult {
PASS,
FAIL,
THROW,
};
struct RunTestParams {
RunTestParams(const ArgumentSpec& spec,
base::StringPiece script_source,
TestResult result)
: spec(spec), script_source(script_source), expected_result(result) {}
const ArgumentSpec& spec;
base::StringPiece script_source;
TestResult expected_result;
base::StringPiece expected_json;
base::StringPiece expected_error;
base::StringPiece expected_thrown_message;
const base::Value* expected_value = nullptr;
bool should_convert_to_base = true;
bool should_convert_to_v8 = false;
V8Validator validate_v8;
};
void ExpectSuccess(const ArgumentSpec& spec,
const std::string& script_source,
const std::string& expected_json_single_quotes) {
RunTestParams params(spec, script_source, TestResult::PASS);
std::string expected_json =
ReplaceSingleQuotes(expected_json_single_quotes);
params.expected_json = expected_json;
RunTest(params);
}
void ExpectSuccess(const ArgumentSpec& spec,
const std::string& script_source,
const base::Value& expected_value) {
RunTestParams params(spec, script_source, TestResult::PASS);
params.expected_value = &expected_value;
RunTest(params);
}
void ExpectSuccess(const ArgumentSpec& spec,
const std::string& script_source,
V8Validator validate_v8) {
RunTestParams params(spec, script_source, TestResult::PASS);
params.should_convert_to_base = false;
params.should_convert_to_v8 = true;
params.validate_v8 = std::move(validate_v8);
RunTest(params);
}
void ExpectSuccessWithNoConversion(const ArgumentSpec& spec,
const std::string& script_source) {
RunTestParams params(spec, script_source, TestResult::PASS);
params.should_convert_to_base = false;
RunTest(params);
}
void ExpectFailure(const ArgumentSpec& spec,
const std::string& script_source,
const std::string& expected_error) {
RunTestParams params(spec, script_source, TestResult::FAIL);
params.expected_error = expected_error;
RunTest(params);
}
void ExpectFailureWithNoConversion(const ArgumentSpec& spec,
const std::string& script_source,
const std::string& expected_error) {
RunTestParams params(spec, script_source, TestResult::FAIL);
params.should_convert_to_base = false;
params.expected_error = expected_error;
RunTest(params);
}
void ExpectThrow(const ArgumentSpec& spec,
const std::string& script_source,
const std::string& expected_thrown_message) {
RunTestParams params(spec, script_source, TestResult::THROW);
params.expected_thrown_message = expected_thrown_message;
RunTest(params);
}
void AddTypeRef(const std::string& id, std::unique_ptr<ArgumentSpec> spec) {
type_refs_.AddSpec(id, std::move(spec));
}
const APITypeReferenceMap& type_refs() const { return type_refs_; }
private:
void RunTest(RunTestParams& params);
APITypeReferenceMap type_refs_;
DISALLOW_COPY_AND_ASSIGN(ArgumentSpecUnitTest);
};
void ArgumentSpecUnitTest::RunTest(RunTestParams& params) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(instance_->isolate());
v8::Local<v8::Context> context =
v8::Local<v8::Context>::New(instance_->isolate(), context_);
v8::TryCatch try_catch(isolate);
v8::Local<v8::Value> val =
V8ValueFromScriptSource(context, params.script_source);
ASSERT_FALSE(val.IsEmpty()) << params.script_source;
std::string error;
std::unique_ptr<base::Value> out_value;
v8::Local<v8::Value> v8_out_value;
bool did_succeed = params.spec.ParseArgument(
context, val, type_refs_,
params.should_convert_to_base ? &out_value : nullptr,
params.should_convert_to_v8 ? &v8_out_value : nullptr, &error);
bool should_succeed = params.expected_result == TestResult::PASS;
ASSERT_EQ(should_succeed, did_succeed)
<< params.script_source << ", " << error;
ASSERT_EQ(did_succeed && params.should_convert_to_base, !!out_value);
ASSERT_EQ(did_succeed && params.should_convert_to_v8,
!v8_out_value.IsEmpty());
bool should_throw = params.expected_result == TestResult::THROW;
ASSERT_EQ(should_throw, try_catch.HasCaught()) << params.script_source;
if (!params.expected_error.empty())
EXPECT_EQ(params.expected_error, error) << params.script_source;
if (should_succeed) {
if (params.should_convert_to_base) {
ASSERT_TRUE(out_value);
if (params.expected_value) {
EXPECT_TRUE(params.expected_value->Equals(out_value.get()))
<< params.script_source;
} else {
EXPECT_EQ(params.expected_json, ValueToString(*out_value));
}
}
if (params.should_convert_to_v8) {
ASSERT_FALSE(v8_out_value.IsEmpty()) << params.script_source;
if (!params.validate_v8.is_null())
std::move(params.validate_v8).Run(v8_out_value);
}
} else if (should_throw) {
EXPECT_EQ(params.expected_thrown_message,
gin::V8ToString(isolate, try_catch.Message()->Get()));
}
}
TEST_F(ArgumentSpecUnitTest, Test) {
using namespace api_errors;
{
ArgumentSpec spec(*ValueFromString("{'type': 'integer'}"));
ExpectSuccess(spec, "1", "1");
ExpectSuccess(spec, "-1", "-1");
ExpectSuccess(spec, "0", "0");
ExpectSuccess(spec, "0.0", "0");
ExpectSuccess(spec, "-0.0", "0");
ExpectSuccess(spec, "-0.", "0");
ExpectSuccess(spec, "-.0", "0");
ExpectSuccess(spec, "-0", "0");
ExpectFailure(spec, "undefined", InvalidType(kTypeInteger, kTypeUndefined));
ExpectFailure(spec, "null", InvalidType(kTypeInteger, kTypeNull));
ExpectFailure(spec, "1.1", InvalidType(kTypeInteger, kTypeDouble));
ExpectFailure(spec, "'foo'", InvalidType(kTypeInteger, kTypeString));
ExpectFailure(spec, "'1'", InvalidType(kTypeInteger, kTypeString));
ExpectFailure(spec, "({})", InvalidType(kTypeInteger, kTypeObject));
ExpectFailure(spec, "[1]", InvalidType(kTypeInteger, kTypeList));
}
{
ArgumentSpec spec(*ValueFromString("{'type': 'number'}"));
ExpectSuccess(spec, "1", "1.0");
ExpectSuccess(spec, "-1", "-1.0");
ExpectSuccess(spec, "0", "0.0");
ExpectSuccess(spec, "1.1", "1.1");
ExpectSuccess(spec, "1.", "1.0");
ExpectSuccess(spec, ".1", "0.1");
ExpectFailure(spec, "undefined", InvalidType(kTypeDouble, kTypeUndefined));
ExpectFailure(spec, "null", InvalidType(kTypeDouble, kTypeNull));
ExpectFailure(spec, "'foo'", InvalidType(kTypeDouble, kTypeString));
ExpectFailure(spec, "'1.1'", InvalidType(kTypeDouble, kTypeString));
ExpectFailure(spec, "({})", InvalidType(kTypeDouble, kTypeObject));
ExpectFailure(spec, "[1.1]", InvalidType(kTypeDouble, kTypeList));
}
{
ArgumentSpec spec(*ValueFromString("{'type': 'integer', 'minimum': 1}"));
ExpectSuccess(spec, "2", "2");
ExpectSuccess(spec, "1", "1");
ExpectFailure(spec, "0", NumberTooSmall(1));
ExpectFailure(spec, "-1", NumberTooSmall(1));
}
{
ArgumentSpec spec(*ValueFromString("{'type': 'integer', 'maximum': 10}"));
ExpectSuccess(spec, "10", "10");
ExpectSuccess(spec, "1", "1");
ExpectFailure(spec, "11", NumberTooLarge(10));
}
{
ArgumentSpec spec(*ValueFromString("{'type': 'string'}"));
ExpectSuccess(spec, "'foo'", "'foo'");
ExpectSuccess(spec, "''", "''");
ExpectFailure(spec, "1", InvalidType(kTypeString, kTypeInteger));
ExpectFailure(spec, "({})", InvalidType(kTypeString, kTypeObject));
ExpectFailure(spec, "['foo']", InvalidType(kTypeString, kTypeList));
}
{
ArgumentSpec spec(
*ValueFromString("{'type': 'string', 'enum': ['foo', 'bar']}"));
std::set<std::string> valid_enums = {"foo", "bar"};
ExpectSuccess(spec, "'foo'", "'foo'");
ExpectSuccess(spec, "'bar'", "'bar'");
ExpectFailure(spec, "['foo']", InvalidType(kTypeString, kTypeList));
ExpectFailure(spec, "'fo'", InvalidEnumValue(valid_enums));
ExpectFailure(spec, "'foobar'", InvalidEnumValue(valid_enums));
ExpectFailure(spec, "'baz'", InvalidEnumValue(valid_enums));
ExpectFailure(spec, "''", InvalidEnumValue(valid_enums));
}
{
ArgumentSpec spec(*ValueFromString(
"{'type': 'string', 'enum': [{'name': 'foo'}, {'name': 'bar'}]}"));
std::set<std::string> valid_enums = {"foo", "bar"};
ExpectSuccess(spec, "'foo'", "'foo'");
ExpectSuccess(spec, "'bar'", "'bar'");
ExpectFailure(spec, "['foo']", InvalidType(kTypeString, kTypeList));
ExpectFailure(spec, "'fo'", InvalidEnumValue(valid_enums));
ExpectFailure(spec, "'foobar'", InvalidEnumValue(valid_enums));
ExpectFailure(spec, "'baz'", InvalidEnumValue(valid_enums));
ExpectFailure(spec, "''", InvalidEnumValue(valid_enums));
}
{
ArgumentSpec spec(*ValueFromString("{'type': 'boolean'}"));
ExpectSuccess(spec, "true", "true");
ExpectSuccess(spec, "false", "false");
ExpectFailure(spec, "1", InvalidType(kTypeBoolean, kTypeInteger));
ExpectFailure(spec, "'true'", InvalidType(kTypeBoolean, kTypeString));
ExpectFailure(spec, "null", InvalidType(kTypeBoolean, kTypeNull));
}
{
ArgumentSpec spec(
*ValueFromString("{'type': 'array', 'items': {'type': 'string'}}"));
ExpectSuccess(spec, "[]", "[]");
ExpectSuccess(spec, "['foo']", "['foo']");
ExpectSuccess(spec, "['foo', 'bar']", "['foo','bar']");
ExpectSuccess(spec, "var x = new Array(); x[0] = 'foo'; x;", "['foo']");
ExpectFailure(spec, "'foo'", InvalidType(kTypeList, kTypeString));
ExpectFailure(spec, "[1, 2]",
IndexError(0u, InvalidType(kTypeString, kTypeInteger)));
ExpectFailure(spec, "['foo', 1]",
IndexError(1u, InvalidType(kTypeString, kTypeInteger)));
ExpectFailure(spec,
"var x = ['a', 'b', 'c'];"
"x[4] = 'd';" // x[3] is undefined, violating the spec.
"x;",
IndexError(3u, InvalidType(kTypeString, kTypeUndefined)));
ExpectThrow(
spec,
"var x = [];"
"Object.defineProperty("
" x, 0,"
" { get: () => { throw new Error('Badness'); } });"
"x;",
"Uncaught Error: Badness");
}
{
const char kObjectSpec[] =
"{"
" 'type': 'object',"
" 'properties': {"
" 'prop1': {'type': 'string'},"
" 'prop2': {'type': 'integer', 'optional': true}"
" }"
"}";
ArgumentSpec spec(*ValueFromString(kObjectSpec));
ExpectSuccess(spec, "({prop1: 'foo', prop2: 2})",
"{'prop1':'foo','prop2':2}");
ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
ExpectSuccess(spec, "({prop1: 'foo', prop2: null})", "{'prop1':'foo'}");
ExpectSuccess(spec, "x = {}; x.prop1 = 'foo'; x;", "{'prop1':'foo'}");
ExpectFailure(
spec, "({prop1: 'foo', prop2: 'bar'})",
PropertyError("prop2", InvalidType(kTypeInteger, kTypeString)));
ExpectFailure(spec, "({prop2: 2})", MissingRequiredProperty("prop1"));
// Unknown properties are not allowed.
ExpectFailure(spec, "({prop1: 'foo', prop2: 2, prop3: 'blah'})",
UnexpectedProperty("prop3"));
// We only consider properties on the object itself, not its prototype
// chain.
ExpectFailure(spec,
"function X() {}\n"
"X.prototype = { prop1: 'foo' };\n"
"var x = new X();\n"
"x;",
MissingRequiredProperty("prop1"));
ExpectFailure(spec,
"function X() {}\n"
"X.prototype = { prop1: 'foo' };\n"
"function Y() { this.__proto__ = X.prototype; }\n"
"var z = new Y();\n"
"z;",
MissingRequiredProperty("prop1"));
// Self-referential fun. Currently we don't have to worry about these much
// because the spec won't match at some point (and V8ValueConverter has
// cycle detection and will fail).
ExpectFailure(
spec, "x = {}; x.prop1 = x; x;",
PropertyError("prop1", InvalidType(kTypeString, kTypeObject)));
ExpectThrow(
spec,
"({ get prop1() { throw new Error('Badness'); }});",
"Uncaught Error: Badness");
ExpectThrow(spec,
"x = {prop1: 'foo'};\n"
"Object.defineProperty(\n"
" x, 'prop2',\n"
" {\n"
" get: () => { throw new Error('Badness'); },\n"
" enumerable: true,\n"
"});\n"
"x;",
"Uncaught Error: Badness");
// By default, properties from Object.defineProperty() aren't enumerable,
// so they will be ignored in our matching.
ExpectSuccess(spec,
"x = {prop1: 'foo'};\n"
"Object.defineProperty(\n"
" x, 'prop2',\n"
" { get: () => { throw new Error('Badness'); } });\n"
"x;",
"{'prop1':'foo'}");
}
{
const char kFunctionSpec[] = "{ 'type': 'function' }";
ArgumentSpec spec(*ValueFromString(kFunctionSpec));
// Functions are serialized as empty dictionaries.
ExpectSuccess(spec, "(function() {})", "{}");
ExpectSuccessWithNoConversion(spec, "(function() {})");
ExpectSuccessWithNoConversion(spec, "(function(a, b) { a(); b(); })");
ExpectSuccessWithNoConversion(spec, "(function(a, b) { a(); b(); })");
ExpectFailureWithNoConversion(spec, "({a: function() {}})",
InvalidType(kTypeFunction, kTypeObject));
ExpectFailureWithNoConversion(spec, "([function() {}])",
InvalidType(kTypeFunction, kTypeList));
ExpectFailureWithNoConversion(spec, "1",
InvalidType(kTypeFunction, kTypeInteger));
}
{
const char kBinarySpec[] = "{ 'type': 'binary' }";
ArgumentSpec spec(*ValueFromString(kBinarySpec));
// Simple case: empty ArrayBuffer -> empty BinaryValue.
ExpectSuccess(spec, "(new ArrayBuffer())",
base::Value(base::Value::Type::BINARY));
{
// A non-empty (but zero-filled) ArrayBufferView.
const char kBuffer[] = {0, 0, 0, 0};
std::unique_ptr<base::Value> expected_value =
base::Value::CreateWithCopiedBuffer(kBuffer, base::size(kBuffer));
ASSERT_TRUE(expected_value);
ExpectSuccessWithNoConversion(spec, "(new Int32Array(2))");
}
{
// Actual data.
const char kBuffer[] = {'p', 'i', 'n', 'g'};
std::unique_ptr<base::Value> expected_value =
base::Value::CreateWithCopiedBuffer(kBuffer, base::size(kBuffer));
ASSERT_TRUE(expected_value);
ExpectSuccess(spec,
"var b = new ArrayBuffer(4);\n"
"var v = new Uint8Array(b);\n"
"var s = 'ping';\n"
"for (var i = 0; i < s.length; ++i)\n"
" v[i] = s.charCodeAt(i);\n"
"b;",
*expected_value);
}
ExpectFailure(spec, "1", InvalidType(kTypeBinary, kTypeInteger));
}
{
const char kAnySpec[] = "{ 'type': 'any' }";
ArgumentSpec spec(*ValueFromString(kAnySpec));
ExpectSuccess(spec, "42", "42");
ExpectSuccess(spec, "'foo'", "'foo'");
ExpectSuccess(spec, "({prop1:'bar'})", "{'prop1':'bar'}");
ExpectSuccess(spec, "[1, 2, 3]", "[1,2,3]");
ExpectSuccess(spec, "[1, 'a']", "[1,'a']");
ExpectSuccess(spec, "null", base::Value());
ExpectSuccess(spec, "({prop1: 'alpha', prop2: null})", "{'prop1':'alpha'}");
ExpectSuccess(spec,
"x = {alpha: 'alpha'};\n"
"y = {beta: 'beta', x: x};\n"
"y;",
"{'beta':'beta','x':{'alpha':'alpha'}}");
// We don't serialize undefined.
// TODO(devlin): This matches current behavior, but should it? Part of the
// problem is that base::Values don't differentiate between undefined and
// null, which is a potentially important distinction. However, this means
// that in serialization of an object {a: 1, foo:undefined}, we lose the
// 'foo' property.
ExpectFailure(spec, "undefined", UnserializableValue());
ExpectSuccess(spec, "({prop1: 1, prop2: undefined})", "{'prop1':1}");
}
}
TEST_F(ArgumentSpecUnitTest, TypeRefsTest) {
using namespace api_errors;
const char kObjectType[] =
"{"
" 'id': 'refObj',"
" 'type': 'object',"
" 'properties': {"
" 'prop1': {'type': 'string'},"
" 'prop2': {'type': 'integer', 'optional': true}"
" }"
"}";
const char kEnumType[] =
"{'id': 'refEnum', 'type': 'string', 'enum': ['alpha', 'beta']}";
AddTypeRef("refObj",
std::make_unique<ArgumentSpec>(*ValueFromString(kObjectType)));
AddTypeRef("refEnum",
std::make_unique<ArgumentSpec>(*ValueFromString(kEnumType)));
std::set<std::string> valid_enums = {"alpha", "beta"};
{
const char kObjectWithRefEnumSpec[] =
"{"
" 'name': 'objWithRefEnum',"
" 'type': 'object',"
" 'properties': {"
" 'e': {'$ref': 'refEnum'},"
" 'sub': {'type': 'integer'}"
" }"
"}";
ArgumentSpec spec(*ValueFromString(kObjectWithRefEnumSpec));
ExpectSuccess(spec, "({e: 'alpha', sub: 1})", "{'e':'alpha','sub':1}");
ExpectSuccess(spec, "({e: 'beta', sub: 1})", "{'e':'beta','sub':1}");
ExpectFailure(spec, "({e: 'gamma', sub: 1})",
PropertyError("e", InvalidEnumValue(valid_enums)));
ExpectFailure(spec, "({e: 'alpha'})", MissingRequiredProperty("sub"));
}
{
const char kObjectWithRefObjectSpec[] =
"{"
" 'name': 'objWithRefObject',"
" 'type': 'object',"
" 'properties': {"
" 'o': {'$ref': 'refObj'}"
" }"
"}";
ArgumentSpec spec(*ValueFromString(kObjectWithRefObjectSpec));
ExpectSuccess(spec, "({o: {prop1: 'foo'}})", "{'o':{'prop1':'foo'}}");
ExpectSuccess(spec, "({o: {prop1: 'foo', prop2: 2}})",
"{'o':{'prop1':'foo','prop2':2}}");
ExpectFailure(
spec, "({o: {prop1: 1}})",
PropertyError("o", PropertyError("prop1", InvalidType(kTypeString,
kTypeInteger))));
}
{
const char kRefEnumListSpec[] =
"{'type': 'array', 'items': {'$ref': 'refEnum'}}";
ArgumentSpec spec(*ValueFromString(kRefEnumListSpec));
ExpectSuccess(spec, "['alpha']", "['alpha']");
ExpectSuccess(spec, "['alpha', 'alpha']", "['alpha','alpha']");
ExpectSuccess(spec, "['alpha', 'beta']", "['alpha','beta']");
ExpectFailure(spec, "['alpha', 'beta', 'gamma']",
IndexError(2u, InvalidEnumValue(valid_enums)));
}
}
TEST_F(ArgumentSpecUnitTest, TypeChoicesTest) {
using namespace api_errors;
{
const char kSimpleChoices[] =
"{'choices': [{'type': 'string'}, {'type': 'integer'}]}";
ArgumentSpec spec(*ValueFromString(kSimpleChoices));
ExpectSuccess(spec, "'alpha'", "'alpha'");
ExpectSuccess(spec, "42", "42");
const char kChoicesType[] = "[string|integer]";
ExpectFailure(spec, "true", InvalidType(kChoicesType, kTypeBoolean));
}
{
const char kComplexChoices[] =
"{"
" 'choices': ["
" {'type': 'array', 'items': {'type': 'string'}},"
" {'type': 'object', 'properties': {'prop1': {'type': 'string'}}}"
" ]"
"}";
ArgumentSpec spec(*ValueFromString(kComplexChoices));
ExpectSuccess(spec, "['alpha']", "['alpha']");
ExpectSuccess(spec, "['alpha', 'beta']", "['alpha','beta']");
ExpectSuccess(spec, "({prop1: 'alpha'})", "{'prop1':'alpha'}");
const char kChoicesType[] = "[array|object]";
ExpectFailure(spec, "({prop1: 1})", InvalidChoice());
ExpectFailure(spec, "'alpha'", InvalidType(kChoicesType, kTypeString));
ExpectFailure(spec, "42", InvalidType(kChoicesType, kTypeInteger));
}
}
TEST_F(ArgumentSpecUnitTest, AdditionalPropertiesTest) {
using namespace api_errors;
{
const char kOnlyAnyAdditionalProperties[] =
"{"
" 'type': 'object',"
" 'additionalProperties': {'type': 'any'}"
"}";
ArgumentSpec spec(*ValueFromString(kOnlyAnyAdditionalProperties));
ExpectSuccess(spec, "({prop1: 'alpha', prop2: 42, prop3: {foo: 'bar'}})",
"{'prop1':'alpha','prop2':42,'prop3':{'foo':'bar'}}");
ExpectSuccess(spec, "({})", "{}");
// Test some crazy keys.
ExpectSuccess(spec,
"var x = {};\n"
"var y = {prop1: 'alpha'};\n"
"y[42] = 'beta';\n"
"y[x] = 'gamma';\n"
"y[undefined] = 'delta';\n"
"y;",
"{'42':'beta','[object Object]':'gamma','prop1':'alpha',"
"'undefined':'delta'}");
// We (typically*, see "Fun case" below) don't serialize properties on an
// object prototype.
ExpectSuccess(spec,
"({\n"
" __proto__: {protoProp: 'proto'},\n"
" instanceProp: 'instance'\n"
"})",
"{'instanceProp':'instance'}");
// Fun case: Remove a property as a result of getting another. Currently,
// we don't check each property with HasOwnProperty() during iteration, so
// Fun case: Remove a property as a result of getting another. Currently,
// we don't check each property with HasOwnProperty() during iteration, so
// we still try to serialize it. But we don't serialize undefined, so in the
// case of the property not being defined on the prototype, this works as
// expected.
ExpectSuccess(spec,
"var x = {};\n"
"Object.defineProperty(\n"
" x, 'alpha',\n"
" {\n"
" enumerable: true,\n"
" get: () => { delete x.omega; return 'alpha'; }\n"
" });\n"
"x.omega = 'omega';\n"
"x;",
"{'alpha':'alpha'}");
// Fun case continued: If an object removes the property, and the property
// *is* present on the prototype, then we serialize the value from the
// prototype. This is inconsistent, but only manifests scripts are doing
// crazy things (and is still safe).
// TODO(devlin): We *could* add a HasOwnProperty() check, in which case
// the result of this call should be {'alpha':'alpha'}.
ExpectSuccess(spec,
"var x = {\n"
" __proto__: { omega: 'different omega' }\n"
"};\n"
"Object.defineProperty(\n"
" x, 'alpha',\n"
" {\n"
" enumerable: true,\n"
" get: () => { delete x.omega; return 'alpha'; }\n"
" });\n"
"x.omega = 'omega';\n"
"x;",
"{'alpha':'alpha','omega':'different omega'}");
}
{
const char kPropertiesAndAnyAdditionalProperties[] =
"{"
" 'type': 'object',"
" 'properties': {"
" 'prop1': {'type': 'string'}"
" },"
" 'additionalProperties': {'type': 'any'}"
"}";
ArgumentSpec spec(*ValueFromString(kPropertiesAndAnyAdditionalProperties));
ExpectSuccess(spec, "({prop1: 'alpha', prop2: 42, prop3: {foo: 'bar'}})",
"{'prop1':'alpha','prop2':42,'prop3':{'foo':'bar'}}");
// Additional properties are optional.
ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
ExpectFailure(spec, "({prop2: 42, prop3: {foo: 'bar'}})",
MissingRequiredProperty("prop1"));
ExpectFailure(
spec, "({prop1: 42})",
PropertyError("prop1", InvalidType(kTypeString, kTypeInteger)));
}
{
const char kTypedAdditionalProperties[] =
"{"
" 'type': 'object',"
" 'additionalProperties': {'type': 'string'}"
"}";
ArgumentSpec spec(*ValueFromString(kTypedAdditionalProperties));
ExpectSuccess(spec, "({prop1: 'alpha', prop2: 'beta', prop3: 'gamma'})",
"{'prop1':'alpha','prop2':'beta','prop3':'gamma'}");
ExpectFailure(
spec, "({prop1: 'alpha', prop2: 42})",
PropertyError("prop2", InvalidType(kTypeString, kTypeInteger)));
}
}
TEST_F(ArgumentSpecUnitTest, InstanceOfTest) {
using namespace api_errors;
{
const char kInstanceOfRegExp[] =
"{"
" 'type': 'object',"
" 'isInstanceOf': 'RegExp'"
"}";
ArgumentSpec spec(*ValueFromString(kInstanceOfRegExp));
ExpectSuccess(spec, "(new RegExp())", "{}");
ExpectSuccess(spec, "({ __proto__: RegExp.prototype })", "{}");
ExpectSuccess(spec,
"(function() {\n"
" function subRegExp() {}\n"
" subRegExp.prototype = { __proto__: RegExp.prototype };\n"
" return new subRegExp();\n"
"})()",
"{}");
ExpectSuccess(spec,
"(function() {\n"
" function RegExp() {}\n"
" return new RegExp();\n"
"})()",
"{}");
ExpectFailure(spec, "({})", NotAnInstance("RegExp"));
ExpectFailure(spec, "('')", InvalidType("RegExp", kTypeString));
ExpectFailure(spec, "('.*')", InvalidType("RegExp", kTypeString));
ExpectFailure(spec, "({ __proto__: Date.prototype })",
NotAnInstance("RegExp"));
}
{
const char kInstanceOfCustomClass[] =
"{"
" 'type': 'object',"
" 'isInstanceOf': 'customClass'"
"}";
ArgumentSpec spec(*ValueFromString(kInstanceOfCustomClass));
ExpectSuccess(spec,
"(function() {\n"
" function customClass() {}\n"
" return new customClass();\n"
"})()",
"{}");
ExpectSuccess(spec,
"(function() {\n"
" function customClass() {}\n"
" function otherClass() {}\n"
" otherClass.prototype = \n"
" { __proto__: customClass.prototype };\n"
" return new otherClass();\n"
"})()",
"{}");
ExpectFailure(spec, "({})", NotAnInstance("customClass"));
ExpectFailure(spec,
"(function() {\n"
" function otherClass() {}\n"
" return new otherClass();\n"
"})()",
NotAnInstance("customClass"));
}
}
TEST_F(ArgumentSpecUnitTest, MinAndMaxLengths) {
using namespace api_errors;
{
const char kMinLengthString[] = "{'type': 'string', 'minLength': 3}";
ArgumentSpec spec(*ValueFromString(kMinLengthString));
ExpectSuccess(spec, "'aaa'", "'aaa'");
ExpectSuccess(spec, "'aaaa'", "'aaaa'");
ExpectFailure(spec, "'aa'", TooFewStringChars(3, 2));
ExpectFailure(spec, "''", TooFewStringChars(3, 0));
}
{
const char kMaxLengthString[] = "{'type': 'string', 'maxLength': 3}";
ArgumentSpec spec(*ValueFromString(kMaxLengthString));
ExpectSuccess(spec, "'aaa'", "'aaa'");
ExpectSuccess(spec, "'aa'", "'aa'");
ExpectSuccess(spec, "''", "''");
ExpectFailure(spec, "'aaaa'", TooManyStringChars(3, 4));
}
{
const char kMinLengthArray[] =
"{'type': 'array', 'items': {'type': 'integer'}, 'minItems': 3}";
ArgumentSpec spec(*ValueFromString(kMinLengthArray));
ExpectSuccess(spec, "[1, 2, 3]", "[1,2,3]");
ExpectSuccess(spec, "[1, 2, 3, 4]", "[1,2,3,4]");
ExpectFailure(spec, "[1, 2]", TooFewArrayItems(3, 2));
ExpectFailure(spec, "[]", TooFewArrayItems(3, 0));
}
{
const char kMaxLengthArray[] =
"{'type': 'array', 'items': {'type': 'integer'}, 'maxItems': 3}";
ArgumentSpec spec(*ValueFromString(kMaxLengthArray));
ExpectSuccess(spec, "[1, 2, 3]", "[1,2,3]");
ExpectSuccess(spec, "[1, 2]", "[1,2]");
ExpectSuccess(spec, "[]", "[]");
ExpectFailure(spec, "[1, 2, 3, 4]", TooManyArrayItems(3, 4));
}
}
TEST_F(ArgumentSpecUnitTest, PreserveNull) {
using namespace api_errors;
{
const char kObjectSpec[] =
"{"
" 'type': 'object',"
" 'additionalProperties': {'type': 'any'},"
" 'preserveNull': true"
"}";
ArgumentSpec spec(*ValueFromString(kObjectSpec));
ExpectSuccess(spec, "({foo: 1, bar: null})", "{'bar':null,'foo':1}");
// Subproperties shouldn't preserve null (if not specified).
ExpectSuccess(spec, "({prop: {subprop1: 'foo', subprop2: null}})",
"{'prop':{'subprop1':'foo'}}");
}
{
const char kObjectSpec[] =
"{"
" 'type': 'object',"
" 'additionalProperties': {'type': 'any', 'preserveNull': true},"
" 'preserveNull': true"
"}";
ArgumentSpec spec(*ValueFromString(kObjectSpec));
ExpectSuccess(spec, "({foo: 1, bar: null})", "{'bar':null,'foo':1}");
// Here, subproperties should preserve null.
ExpectSuccess(spec, "({prop: {subprop1: 'foo', subprop2: null}})",
"{'prop':{'subprop1':'foo','subprop2':null}}");
}
{
const char kObjectSpec[] =
"{"
" 'type': 'object',"
" 'properties': {'prop1': {'type': 'string', 'optional': true}},"
" 'preserveNull': true"
"}";
ArgumentSpec spec(*ValueFromString(kObjectSpec));
ExpectSuccess(spec, "({})", "{}");
ExpectSuccess(spec, "({prop1: null})", "{'prop1':null}");
ExpectSuccess(spec, "({prop1: 'foo'})", "{'prop1':'foo'}");
// Undefined should not be preserved.
ExpectSuccess(spec, "({prop1: undefined})", "{}");
// preserveNull shouldn't affect normal parsing restrictions.
ExpectFailure(
spec, "({prop1: 1})",
PropertyError("prop1", InvalidType(kTypeString, kTypeInteger)));
}
}
TEST_F(ArgumentSpecUnitTest, NaNFun) {
using namespace api_errors;
{
const char kAnySpec[] = "{'type': 'any'}";
ArgumentSpec spec(*ValueFromString(kAnySpec));
ExpectFailure(spec, "NaN", UnserializableValue());
}
{
const char kObjectWithAnyPropertiesSpec[] =
"{'type': 'object', 'additionalProperties': {'type': 'any'}}";
ArgumentSpec spec(*ValueFromString(kObjectWithAnyPropertiesSpec));
ExpectSuccess(spec, "({foo: NaN, bar: 'baz'})", "{'bar':'baz'}");
}
}
TEST_F(ArgumentSpecUnitTest, GetTypeName) {
struct {
ArgumentType type;
const char* expected_type_name;
} simple_cases[] = {
{ArgumentType::BOOLEAN, api_errors::kTypeBoolean},
{ArgumentType::INTEGER, api_errors::kTypeInteger},
{ArgumentType::OBJECT, api_errors::kTypeObject},
{ArgumentType::LIST, api_errors::kTypeList},
{ArgumentType::BINARY, api_errors::kTypeBinary},
{ArgumentType::FUNCTION, api_errors::kTypeFunction},
{ArgumentType::ANY, api_errors::kTypeAny},
};
for (const auto& test_case : simple_cases) {
ArgumentSpec spec(test_case.type);
EXPECT_EQ(test_case.expected_type_name, spec.GetTypeName());
}
{
const char kRefName[] = "someRef";
ArgumentSpec ref_spec(ArgumentType::REF);
ref_spec.set_ref(kRefName);
EXPECT_EQ(kRefName, ref_spec.GetTypeName());
}
{
std::vector<std::unique_ptr<ArgumentSpec>> choices;
choices.push_back(std::make_unique<ArgumentSpec>(ArgumentType::INTEGER));
choices.push_back(std::make_unique<ArgumentSpec>(ArgumentType::STRING));
ArgumentSpec choices_spec(ArgumentType::CHOICES);
choices_spec.set_choices(std::move(choices));
EXPECT_EQ("[integer|string]", choices_spec.GetTypeName());
}
}
TEST_F(ArgumentSpecUnitTest, V8Conversion) {
{
// Sanity check: a simple conversion.
ArgumentSpec spec(ArgumentType::INTEGER);
ExpectSuccess(spec, "1", base::BindOnce([](v8::Local<v8::Value> value) {
ASSERT_TRUE(value->IsInt32());
EXPECT_EQ(1, value.As<v8::Int32>()->Value());
}));
// The conversion should handle the -0 value (which is considered an
// integer but stored in v8 has a number) by converting it to a 0 integer.
ExpectSuccess(spec, "-0", base::BindOnce([](v8::Local<v8::Value> value) {
ASSERT_TRUE(value->IsInt32());
EXPECT_EQ(0, value.As<v8::Int32>()->Value());
}));
}
{
std::unique_ptr<ArgumentSpec> prop =
ArgumentSpecBuilder(ArgumentType::STRING, "str").Build();
std::unique_ptr<ArgumentSpec> spec =
ArgumentSpecBuilder(ArgumentType::OBJECT, "obj")
.AddProperty("str", std::move(prop))
.Build();
// The conversion to a v8 value should handle tricky cases like this, where
// a subtle getter would invalidate expectations if the value was passed
// directly. Since the conversion creates a new object (free of any sneaky
// getters), we don't need to repeatedly check for types.
constexpr char kTrickyInterceptors[] = R"(
({
get str() {
if (this.checkedOnce)
return 42;
this.checkedOnce = true;
return 'a string';
}
}))";
ExpectSuccess(*spec, kTrickyInterceptors,
base::BindOnce([](v8::Local<v8::Value> value) {
ASSERT_TRUE(value->IsObject());
v8::Local<v8::Object> object = value.As<v8::Object>();
v8::Local<v8::Context> context = object->CreationContext();
gin::Dictionary dict(context->GetIsolate(), object);
std::string result;
ASSERT_TRUE(dict.Get("str", &result));
EXPECT_EQ("a string", result);
ASSERT_TRUE(dict.Get("str", &result));
// The value should remain constant (even though there's a
// subtle getter).
EXPECT_EQ("a string", result);
}));
}
{
std::unique_ptr<ArgumentSpec> prop =
ArgumentSpecBuilder(ArgumentType::STRING, "str").MakeOptional().Build();
std::unique_ptr<ArgumentSpec> spec =
ArgumentSpecBuilder(ArgumentType::OBJECT, "obj")
.AddProperty("str", std::move(prop))
.Build();
// The conversion to a v8 value should also protect set an undefined value
// on the result value for any absent optional properties. This protects
// against cases where an Object.prototype getter would be invoked when a
// handler tried to check the value of an argument.
constexpr const char kMessWithObjectPrototype[] =
R"((function() {
Object.defineProperty(
Object.prototype, 'str',
{ get() { throw new Error('tricked!'); } });
}))";
v8::HandleScope handle_scope(instance_->isolate());
v8::Local<v8::Context> context =
v8::Local<v8::Context>::New(instance_->isolate(), context_);
v8::Local<v8::Function> mess_with_proto =
FunctionFromString(context, kMessWithObjectPrototype);
RunFunction(mess_with_proto, context, 0, nullptr);
ExpectSuccess(*spec, "({})", base::BindOnce([](v8::Local<v8::Value> value) {
ASSERT_TRUE(value->IsObject());
v8::Local<v8::Object> object = value.As<v8::Object>();
v8::Local<v8::Context> context = object->CreationContext();
// We expect a null prototype to ensure we avoid tricky getters/setters on
// the Object prototype.
EXPECT_TRUE(object->GetPrototype()->IsNull());
gin::Dictionary dict(context->GetIsolate(), object);
v8::Local<v8::Value> result;
ASSERT_TRUE(dict.Get("str", &result));
EXPECT_TRUE(result->IsUndefined());
}));
}
{
std::unique_ptr<ArgumentSpec> prop =
ArgumentSpecBuilder(ArgumentType::STRING, "prop")
.MakeOptional()
.Build();
std::unique_ptr<ArgumentSpec> preserve_null_spec =
ArgumentSpecBuilder(ArgumentType::OBJECT, "spec")
.AddProperty("prop", std::move(prop))
.PreserveNull()
.Build();
ExpectSuccess(
*preserve_null_spec, "({prop: null})",
base::BindOnce([](v8::Local<v8::Value> value) {
ASSERT_TRUE(value->IsObject());
v8::Local<v8::Object> object = value.As<v8::Object>();
v8::Local<v8::Context> context = object->CreationContext();
v8::Local<v8::Value> prop =
object
->Get(context, gin::StringToV8(context->GetIsolate(), "prop"))
.ToLocalChecked();
EXPECT_TRUE(prop->IsNull());
}));
}
}
// Certain argument types (any, binary, and function) will be passed through
// directly to the v8 arguments because we won't be able to parse them properly.
TEST_F(ArgumentSpecUnitTest, TestV8ValuePassedThrough) {
v8::Isolate* isolate = instance_->isolate();
v8::HandleScope handle_scope(instance_->isolate());
v8::Local<v8::Context> context =
v8::Local<v8::Context>::New(instance_->isolate(), context_);
v8::TryCatch try_catch(isolate);
auto test_is_same_value = [this, context](const ArgumentSpec& spec,
const char* value_str) {
SCOPED_TRACE(value_str);
v8::Local<v8::Value> value_in = V8ValueFromScriptSource(context, value_str);
v8::Local<v8::Value> value_out;
std::string error;
ASSERT_TRUE(spec.ParseArgument(context, value_in, type_refs(), nullptr,
&value_out, &error));
ASSERT_FALSE(value_out.IsEmpty());
EXPECT_EQ(value_in, value_out);
};
// Functions are passed directly because we reply directly to the passed-in
// function.
test_is_same_value(ArgumentSpec(ArgumentType::FUNCTION), "(function() {})");
// 'Any' and 'Binary' arguments are passed directly because we can't know if
// we can copy them safely, accurately, and efficiently.
test_is_same_value(ArgumentSpec(ArgumentType::ANY), "({foo: 'bar'})");
test_is_same_value(ArgumentSpec(ArgumentType::BINARY), "(new ArrayBuffer())");
// See comments in ArgumentSpec::ParseArgumentToObject() for why these are
// passed directly.
{
std::unique_ptr<ArgumentSpec> instance_of_spec =
ArgumentSpecBuilder(ArgumentType::OBJECT, "instance_of")
.SetInstanceOf("RegExp")
.Build();
test_is_same_value(*instance_of_spec, "(new RegExp('hi'))");
}
{
std::unique_ptr<ArgumentSpec> additional_properties_spec =
ArgumentSpecBuilder(ArgumentType::OBJECT, "additional props")
.SetAdditionalProperties(
std::make_unique<ArgumentSpec>(ArgumentType::ANY))
.Build();
test_is_same_value(*additional_properties_spec, "({foo: 'bar'})");
}
}
} // namespace extensions